From ba1948af98eef7e5909c88d5818bc77d94dce4f5 Mon Sep 17 00:00:00 2001 From: Manish Maharjan <112875432+mmaharjan-ccdc@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:03:17 +0100 Subject: [PATCH 01/15] SYS-8665 base changes --- .pre-commit-hooks.yaml | 24 ++++++++++++++++++ action.yml | 24 +++++++++++++++++- main/copywrite/.copywrite.hcl | 34 ++++++++++++++++++++++++++ main/copywrite/headers/ccdc_hash.tmpl | 15 ++++++++++++ main/copywrite/headers/ccdc_slash.tmpl | 15 ++++++++++++ main/hooks/copywrite_check.sh | 24 ++++++++++++++++++ main/hooks/copywrite_fix.sh | 24 ++++++++++++++++++ 7 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 .pre-commit-hooks.yaml create mode 100644 main/copywrite/.copywrite.hcl create mode 100644 main/copywrite/headers/ccdc_hash.tmpl create mode 100644 main/copywrite/headers/ccdc_slash.tmpl create mode 100644 main/hooks/copywrite_check.sh create mode 100644 main/hooks/copywrite_fix.sh diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..d8807ea --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# +# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre +# (CCDC) of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. +# This code may not be used, reproduced, translated, modified, disassembled or +# copied, except in accordance with a valid licence agreement with CCDC and may +# not be disclosed or redistributed in any form, either in whole or in part, to +# any third party. All copies of this code made in accordance with a valid +# licence agreement as referred to above must contain this copyright notice. +# +# No representations, warranties, or liabilities are expressed or implied in the +# supply of this code by CCDC, its servants or agents, except where such +# exclusion or limitation is prohibited, void or unenforceable under governing +# law. +# +set -euo pipefail +command -v copywrite >/dev/null 2>&1 || { echo "copywrite not found on PATH"; exit 1; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HOOK_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +CONFIG_PATH="${HOOK_ROOT}/main/copywrite/.copywrite.hcl" + +export COPYWRITE_HOOK_ROOT="${HOOK_ROOT}" +copywrite headers --config="${CONFIG_PATH}" \ No newline at end of file diff --git a/action.yml b/action.yml index 3d7bbd1..567a9f7 100644 --- a/action.yml +++ b/action.yml @@ -1,3 +1,17 @@ +# +# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre +# (CCDC) of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. +# This code may not be used, reproduced, translated, modified, disassembled or +# copied, except in accordance with a valid licence agreement with CCDC and may +# not be disclosed or redistributed in any form, either in whole or in part, to +# any third party. All copies of this code made in accordance with a valid +# licence agreement as referred to above must contain this copyright notice. +# +# No representations, warranties, or liabilities are expressed or implied in the +# supply of this code by CCDC, its servants or agents, except where such +# exclusion or limitation is prohibited, void or unenforceable under governing +# law. +# name: CCDC File Checks author: CCDC description: Check changed files for compliance @@ -8,7 +22,15 @@ inputs: runs: using: "composite" steps: - - run: python3 $GITHUB_ACTION_PATH/main.py + - name: Install copywrite + uses: hashicorp/setup-copywrite@v1.1.3 + + - name: Validate Header Compliance + run: copywrite headers --config "$GITHUB_ACTION_PATH/main/copywrite/.copywrite.hcl" --plan + shell: bash + + - name: Run file compliance check + run: python3 $GITHUB_ACTION_PATH/main.py shell: bash env: INPUT_COMMITMESSAGE: ${{ inputs.commitMessage }} diff --git a/main/copywrite/.copywrite.hcl b/main/copywrite/.copywrite.hcl new file mode 100644 index 0000000..8ecea19 --- /dev/null +++ b/main/copywrite/.copywrite.hcl @@ -0,0 +1,34 @@ +schema_version = 1 + +project { + copyright_holder = "The Cambridge Crystallographic Data Centre (CCDC)" + + header_ignore = [ + ".git/**", + ".github/**", + "**/bin/**", + "**/obj/**", + "**/packages/**", + "**/node_modules/**", + "**/dist/**", + "**/build/**", + "**/.venv/**", + "**/venv/**", + "**/__pycache__/**", + "**/*.Designer.cs", + "**/*.g.cs", + "**/*.generated.*", + "**/*.min.js", + "**/*.lock", + ] +} + +rule { + paths = ["**/*.py", "**/*.sh", "**/*.bash", "**/*.yaml", "**/*.yml"] + license_header = "${COPYWRITE_HOOK_ROOT}/main/copywrite/headers/ccdc_hash.tmpl" +} + +rule { + paths = ["**/*.js", "**/*.ts", "**/*.cs", "**/*.cpp", "**/*.cxx", "**/*.cc", "**/*.h", "**/*.hpp"] + license_header = "${COPYWRITE_HOOK_ROOT}/main/copywrite/headers/ccdc_slash.tmpl" +} \ No newline at end of file diff --git a/main/copywrite/headers/ccdc_hash.tmpl b/main/copywrite/headers/ccdc_hash.tmpl new file mode 100644 index 0000000..a4315d4 --- /dev/null +++ b/main/copywrite/headers/ccdc_hash.tmpl @@ -0,0 +1,15 @@ +# START_LICENSE_TEXT +# This code is Copyright (C) {{ .Year }} The Cambridge Crystallographic Data Centre (CCDC) +# of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This +# code may not be used, reproduced, translated, modified, disassembled or +# copied, except in accordance with a valid licence agreement with CCDC and +# may NOT be disclosed or redistributed in any form, either in whole or in +# part, to any third party. All copies of this code made in accordance with a +# valid licence agreement as referred to above must contain this copyright +# notice. +# +# No representations, warranties, or liabilities are expressed or implied in +# the supply of this code by CCDC, its servants or agents, except where such +# exclusion or limitation is prohibited, void or unenforceable under governing +# law. +# END_LICENSE_TEXT \ No newline at end of file diff --git a/main/copywrite/headers/ccdc_slash.tmpl b/main/copywrite/headers/ccdc_slash.tmpl new file mode 100644 index 0000000..0882de2 --- /dev/null +++ b/main/copywrite/headers/ccdc_slash.tmpl @@ -0,0 +1,15 @@ +// START_LICENSE_TEXT +// This code is Copyright (C) {{ .Year }} The Cambridge Crystallographic Data Centre (CCDC) +// of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This +// code may not be used, reproduced, translated, modified, disassembled or +// copied, except in accordance with a valid licence agreement with CCDC and +// may NOT be disclosed or redistributed in any form, either in whole or in +// part, to any third party. All copies of this code made in accordance with a +// valid licence agreement as referred to above must contain this copyright +// notice. +// +// No representations, warranties, or liabilities are expressed or implied in +// the supply of this code by CCDC, its servants or agents, except where such +// exclusion or limitation is prohibited, void or unenforceable under governing +// law. +// END_LICENSE_TEXT \ No newline at end of file diff --git a/main/hooks/copywrite_check.sh b/main/hooks/copywrite_check.sh new file mode 100644 index 0000000..15389bf --- /dev/null +++ b/main/hooks/copywrite_check.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# +# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre +# (CCDC) of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. +# This code may not be used, reproduced, translated, modified, disassembled or +# copied, except in accordance with a valid licence agreement with CCDC and may +# not be disclosed or redistributed in any form, either in whole or in part, to +# any third party. All copies of this code made in accordance with a valid +# licence agreement as referred to above must contain this copyright notice. +# +# No representations, warranties, or liabilities are expressed or implied in the +# supply of this code by CCDC, its servants or agents, except where such +# exclusion or limitation is prohibited, void or unenforceable under governing +# law. +# +set -euo pipefail +command -v copywrite >/dev/null 2>&1 || { echo "copywrite not found on PATH"; exit 1; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HOOK_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +CONFIG_PATH="${HOOK_ROOT}/main/copywrite/.copywrite.hcl" + +export COPYWRITE_HOOK_ROOT="${HOOK_ROOT}" +copywrite headers --config="${CONFIG_PATH}" --plan \ No newline at end of file diff --git a/main/hooks/copywrite_fix.sh b/main/hooks/copywrite_fix.sh new file mode 100644 index 0000000..d8807ea --- /dev/null +++ b/main/hooks/copywrite_fix.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# +# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre +# (CCDC) of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. +# This code may not be used, reproduced, translated, modified, disassembled or +# copied, except in accordance with a valid licence agreement with CCDC and may +# not be disclosed or redistributed in any form, either in whole or in part, to +# any third party. All copies of this code made in accordance with a valid +# licence agreement as referred to above must contain this copyright notice. +# +# No representations, warranties, or liabilities are expressed or implied in the +# supply of this code by CCDC, its servants or agents, except where such +# exclusion or limitation is prohibited, void or unenforceable under governing +# law. +# +set -euo pipefail +command -v copywrite >/dev/null 2>&1 || { echo "copywrite not found on PATH"; exit 1; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HOOK_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +CONFIG_PATH="${HOOK_ROOT}/main/copywrite/.copywrite.hcl" + +export COPYWRITE_HOOK_ROOT="${HOOK_ROOT}" +copywrite headers --config="${CONFIG_PATH}" \ No newline at end of file From 1fa442c63576c3005afd6976b004ff80d51cc4f6 Mon Sep 17 00:00:00 2001 From: Manish Maharjan <112875432+mmaharjan-ccdc@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:31:04 +0100 Subject: [PATCH 02/15] SYS-8665 improvements and readme updates --- .pre-commit-hooks.yaml | 22 ++++++---- README.md | 80 +++++++++++++++++++++++++++++++++-- action.yml | 18 +++++--- main/copywrite/.copywrite.hcl | 2 + main/hooks/copywrite_check.sh | 4 +- main/hooks/copywrite_fix.sh | 4 +- 6 files changed, 110 insertions(+), 20 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index d8807ea..c938275 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -12,13 +12,19 @@ # supply of this code by CCDC, its servants or agents, except where such # exclusion or limitation is prohibited, void or unenforceable under governing # law. -# -set -euo pipefail -command -v copywrite >/dev/null 2>&1 || { echo "copywrite not found on PATH"; exit 1; } -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -HOOK_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" -CONFIG_PATH="${HOOK_ROOT}/main/copywrite/.copywrite.hcl" +- id: copywrite-check + name: copywrite-check + description: Validate copyright and license header compliance using Copywrite + entry: main/hooks/copywrite_check.sh + language: script + pass_filenames: false + always_run: true -export COPYWRITE_HOOK_ROOT="${HOOK_ROOT}" -copywrite headers --config="${CONFIG_PATH}" \ No newline at end of file +- id: copywrite-fix + name: copywrite-fix + description: Automatically format and add copyright and license headers using Copywrite + entry: main/hooks/copywrite_fix.sh + language: script + pass_filenames: false + always_run: true diff --git a/README.md b/README.md index f38d38c..c500015 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ The commit will be flagged if it includes certain text files with: * Tabs * Missing terminating newline for certain files * Certain C++ #include patterns and std::exception +* Missing or non-compliant CCDC copyright and license headers (via HashiCorp Copywrite) The commit will also be flagged if the commit message does not include a Jira ID (unless marked with NO_JIRA or a Copilot Autofix co-author line), or if the @@ -62,9 +63,82 @@ A set of hooks include: * pre-commit * pre-merge-commit -## Setting up -1. Clone this repo -1. `git config --global core.hooksPath /main` +## Setting up Core Git Hooks + +1. Clone this repository. +2. Configure Git to use the hooks: + +```bash +git config --global core.hooksPath /main +``` + +This enables the CCDC commit hooks for all repositories on your machine. + +## Using with `pre-commit` + +This repository also provides hooks compatible with the +[`pre-commit`](https://pre-commit.com/) framework for managing copyright +headers using [copywrite (github link)](https://github.com/hashicorp/copywrite). + +### Available Hooks + +* **`copywrite-fix`** *(recommended for local development)*: + Automatically inserts or updates the CCDC copyright and licence headers + in newly added or modified files. + +* **`copywrite-check`**: + Validates copyright and licence headers across tracked files and fails if + any files are non-compliant. This hook is well suited for CI/CD pipelines + and verification workflows. + +### Example Configuration + +Add the following to your `.pre-commit-config.yaml`: + +```yaml +repos: + - repo: https://github.com/ccdc-opensource/commit-hooks + rev: + hooks: + # Automatically inserts or updates headers + - id: copywrite-fix + + # Optional: validates compliance after formatting + # - id: copywrite-check +``` + +### Copywrite Prerequisite + +The `copywrite-check` and `copywrite-fix` hooks require the `copywrite` +CLI to be available on your `PATH`. + +**macOS / Linux (Homebrew)** + +```bash +brew install hashicorp/tap/copywrite +``` + +**Go** + +```bash +go install github.com/hashicorp/copywrite@latest +``` + +**Direct download** + +Binary releases are available from: + +https://github.com/hashicorp/copywrite/releases + +### Recommended Usage + +For the best developer experience: + +* Use **`copywrite-fix`** locally to automatically insert or update headers. +* Use **`copywrite-check`** in CI/CD pipelines to enforce compliance. + +This ensures that copyright and licence headers are automatically maintained +while also preventing non-compliant changes from being merged. ## Recommended settings ### To ensure the line endings are correctly converted: diff --git a/action.yml b/action.yml index 567a9f7..7f7dd89 100644 --- a/action.yml +++ b/action.yml @@ -20,20 +20,28 @@ inputs: description: 'The commit message' required: true runs: - using: "composite" + using: composite steps: - name: Install copywrite uses: hashicorp/setup-copywrite@v1.1.3 - name: Validate Header Compliance - run: copywrite headers --config "$GITHUB_ACTION_PATH/main/copywrite/.copywrite.hcl" --plan shell: bash + env: + COPYWRITE_HOOK_ROOT: ${{ github.action_path }} + run: | + copywrite --version + copywrite headers \ + --config "$GITHUB_ACTION_PATH/main/copywrite/.copywrite.hcl" \ + --plan - name: Run file compliance check - run: python3 $GITHUB_ACTION_PATH/main.py shell: bash env: INPUT_COMMITMESSAGE: ${{ inputs.commitMessage }} + run: | + python3 "$GITHUB_ACTION_PATH/main.py" + branding: - icon: 'check-square' - color: 'green' + icon: check-square + color: green diff --git a/main/copywrite/.copywrite.hcl b/main/copywrite/.copywrite.hcl index 8ecea19..5c415e9 100644 --- a/main/copywrite/.copywrite.hcl +++ b/main/copywrite/.copywrite.hcl @@ -6,6 +6,8 @@ project { header_ignore = [ ".git/**", ".github/**", + "test/**", + "tests/**", "**/bin/**", "**/obj/**", "**/packages/**", diff --git a/main/hooks/copywrite_check.sh b/main/hooks/copywrite_check.sh index 15389bf..c5aa2a1 100644 --- a/main/hooks/copywrite_check.sh +++ b/main/hooks/copywrite_check.sh @@ -18,7 +18,7 @@ command -v copywrite >/dev/null 2>&1 || { echo "copywrite not found on PATH"; ex SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HOOK_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" -CONFIG_PATH="${HOOK_ROOT}/main/copywrite/.copywrite.hcl" +CONFIG_PATH="${SCRIPT_DIR}/../copywrite/.copywrite.hcl" export COPYWRITE_HOOK_ROOT="${HOOK_ROOT}" -copywrite headers --config="${CONFIG_PATH}" --plan \ No newline at end of file +copywrite headers --config="${CONFIG_PATH}" --plan diff --git a/main/hooks/copywrite_fix.sh b/main/hooks/copywrite_fix.sh index d8807ea..c1ed967 100644 --- a/main/hooks/copywrite_fix.sh +++ b/main/hooks/copywrite_fix.sh @@ -18,7 +18,7 @@ command -v copywrite >/dev/null 2>&1 || { echo "copywrite not found on PATH"; ex SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HOOK_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" -CONFIG_PATH="${HOOK_ROOT}/main/copywrite/.copywrite.hcl" +CONFIG_PATH="${SCRIPT_DIR}/../copywrite/.copywrite.hcl" export COPYWRITE_HOOK_ROOT="${HOOK_ROOT}" -copywrite headers --config="${CONFIG_PATH}" \ No newline at end of file +copywrite headers --config="${CONFIG_PATH}" From 14798f711994d0e8d5473b0f48122e9b3d7c78b8 Mon Sep 17 00:00:00 2001 From: Manish Maharjan <112875432+mmaharjan-ccdc@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:33:10 +0100 Subject: [PATCH 03/15] SYS-8665 minor updates to readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c500015..a33864a 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ This enables the CCDC commit hooks for all repositories on your machine. This repository also provides hooks compatible with the [`pre-commit`](https://pre-commit.com/) framework for managing copyright -headers using [copywrite (github link)](https://github.com/hashicorp/copywrite). +headers using [HashiCorp Copywrite](https://github.com/hashicorp/copywrite). ### Available Hooks @@ -128,7 +128,7 @@ go install github.com/hashicorp/copywrite@latest Binary releases are available from: -https://github.com/hashicorp/copywrite/releases +[HashiCorp Copywrite Releases](https://github.com/hashicorp/copywrite/releases) ### Recommended Usage From d3b70fbaa1ac74d88e6029b59b5b2d5607a8d1a7 Mon Sep 17 00:00:00 2001 From: Manish Maharjan <112875432+mmaharjan-ccdc@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:55:21 +0100 Subject: [PATCH 04/15] SYS-8665 add copywrite message at top --- .pre-commit-hooks.yaml | 21 +++++++++++---------- action.yml | 19 ++++++++++--------- main.py | 15 +++++++++++++++ main/commit-msg.py | 15 +++++++++++++++ main/copywrite/headers/ccdc_hash.tmpl | 2 ++ main/copywrite/headers/ccdc_slash.tmpl | 2 ++ main/githooks.py | 15 +++++++++++++++ main/hooks/copywrite_check.sh | 19 ++++++++++--------- main/hooks/copywrite_fix.sh | 19 ++++++++++--------- main/pre-commit.py | 15 +++++++++++++++ main/pre-merge-commit.py | 15 +++++++++++++++ 11 files changed, 120 insertions(+), 37 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index c938275..db79dd9 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -1,17 +1,18 @@ -#!/usr/bin/env bash # -# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre -# (CCDC) of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. -# This code may not be used, reproduced, translated, modified, disassembled or -# copied, except in accordance with a valid licence agreement with CCDC and may -# not be disclosed or redistributed in any form, either in whole or in part, to -# any third party. All copies of this code made in accordance with a valid -# licence agreement as referred to above must contain this copyright notice. +# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre (CCDC) +# of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This +# code may not be used, reproduced, translated, modified, disassembled or +# copied, except in accordance with a valid licence agreement with CCDC and +# may NOT be disclosed or redistributed in any form, either in whole or in +# part, to any third party. All copies of this code made in accordance with a +# valid licence agreement as referred to above must contain this copyright +# notice. # -# No representations, warranties, or liabilities are expressed or implied in the -# supply of this code by CCDC, its servants or agents, except where such +# No representations, warranties, or liabilities are expressed or implied in +# the supply of this code by CCDC, its servants or agents, except where such # exclusion or limitation is prohibited, void or unenforceable under governing # law. +# - id: copywrite-check name: copywrite-check diff --git a/action.yml b/action.yml index 7f7dd89..d7b7afd 100644 --- a/action.yml +++ b/action.yml @@ -1,14 +1,15 @@ # -# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre -# (CCDC) of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. -# This code may not be used, reproduced, translated, modified, disassembled or -# copied, except in accordance with a valid licence agreement with CCDC and may -# not be disclosed or redistributed in any form, either in whole or in part, to -# any third party. All copies of this code made in accordance with a valid -# licence agreement as referred to above must contain this copyright notice. +# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre (CCDC) +# of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This +# code may not be used, reproduced, translated, modified, disassembled or +# copied, except in accordance with a valid licence agreement with CCDC and +# may NOT be disclosed or redistributed in any form, either in whole or in +# part, to any third party. All copies of this code made in accordance with a +# valid licence agreement as referred to above must contain this copyright +# notice. # -# No representations, warranties, or liabilities are expressed or implied in the -# supply of this code by CCDC, its servants or agents, except where such +# No representations, warranties, or liabilities are expressed or implied in +# the supply of this code by CCDC, its servants or agents, except where such # exclusion or limitation is prohibited, void or unenforceable under governing # law. # diff --git a/main.py b/main.py index ba28722..d6cc21a 100755 --- a/main.py +++ b/main.py @@ -1,4 +1,19 @@ #!/usr/bin/env python3 +# +# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre (CCDC) +# of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This +# code may not be used, reproduced, translated, modified, disassembled or +# copied, except in accordance with a valid licence agreement with CCDC and +# may NOT be disclosed or redistributed in any form, either in whole or in +# part, to any third party. All copies of this code made in accordance with a +# valid licence agreement as referred to above must contain this copyright +# notice. +# +# No representations, warranties, or liabilities are expressed or implied in +# the supply of this code by CCDC, its servants or agents, except where such +# exclusion or limitation is prohibited, void or unenforceable under governing +# law. +# ''' This is a github action entry point. diff --git a/main/commit-msg.py b/main/commit-msg.py index 0de1961..c0f5bfe 100755 --- a/main/commit-msg.py +++ b/main/commit-msg.py @@ -1,4 +1,19 @@ #!/usr/bin/env python3 +# +# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre (CCDC) +# of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This +# code may not be used, reproduced, translated, modified, disassembled or +# copied, except in accordance with a valid licence agreement with CCDC and +# may NOT be disclosed or redistributed in any form, either in whole or in +# part, to any third party. All copies of this code made in accordance with a +# valid licence agreement as referred to above must contain this copyright +# notice. +# +# No representations, warranties, or liabilities are expressed or implied in +# the supply of this code by CCDC, its servants or agents, except where such +# exclusion or limitation is prohibited, void or unenforceable under governing +# law. +# ''' A hook to check commit massage. diff --git a/main/copywrite/headers/ccdc_hash.tmpl b/main/copywrite/headers/ccdc_hash.tmpl index a4315d4..5adb3cd 100644 --- a/main/copywrite/headers/ccdc_hash.tmpl +++ b/main/copywrite/headers/ccdc_hash.tmpl @@ -1,4 +1,5 @@ # START_LICENSE_TEXT +# # This code is Copyright (C) {{ .Year }} The Cambridge Crystallographic Data Centre (CCDC) # of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This # code may not be used, reproduced, translated, modified, disassembled or @@ -12,4 +13,5 @@ # the supply of this code by CCDC, its servants or agents, except where such # exclusion or limitation is prohibited, void or unenforceable under governing # law. +# # END_LICENSE_TEXT \ No newline at end of file diff --git a/main/copywrite/headers/ccdc_slash.tmpl b/main/copywrite/headers/ccdc_slash.tmpl index 0882de2..22357f7 100644 --- a/main/copywrite/headers/ccdc_slash.tmpl +++ b/main/copywrite/headers/ccdc_slash.tmpl @@ -1,4 +1,5 @@ // START_LICENSE_TEXT +// // This code is Copyright (C) {{ .Year }} The Cambridge Crystallographic Data Centre (CCDC) // of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This // code may not be used, reproduced, translated, modified, disassembled or @@ -12,4 +13,5 @@ // the supply of this code by CCDC, its servants or agents, except where such // exclusion or limitation is prohibited, void or unenforceable under governing // law. +// // END_LICENSE_TEXT \ No newline at end of file diff --git a/main/githooks.py b/main/githooks.py index 35fba84..08595e9 100644 --- a/main/githooks.py +++ b/main/githooks.py @@ -1,4 +1,19 @@ #!/usr/bin/env python3 +# +# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre (CCDC) +# of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This +# code may not be used, reproduced, translated, modified, disassembled or +# copied, except in accordance with a valid licence agreement with CCDC and +# may NOT be disclosed or redistributed in any form, either in whole or in +# part, to any third party. All copies of this code made in accordance with a +# valid licence agreement as referred to above must contain this copyright +# notice. +# +# No representations, warranties, or liabilities are expressed or implied in +# the supply of this code by CCDC, its servants or agents, except where such +# exclusion or limitation is prohibited, void or unenforceable under governing +# law. +# ''' Module for a git hook. diff --git a/main/hooks/copywrite_check.sh b/main/hooks/copywrite_check.sh index c5aa2a1..7237707 100644 --- a/main/hooks/copywrite_check.sh +++ b/main/hooks/copywrite_check.sh @@ -1,15 +1,16 @@ #!/usr/bin/env bash # -# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre -# (CCDC) of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. -# This code may not be used, reproduced, translated, modified, disassembled or -# copied, except in accordance with a valid licence agreement with CCDC and may -# not be disclosed or redistributed in any form, either in whole or in part, to -# any third party. All copies of this code made in accordance with a valid -# licence agreement as referred to above must contain this copyright notice. +# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre (CCDC) +# of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This +# code may not be used, reproduced, translated, modified, disassembled or +# copied, except in accordance with a valid licence agreement with CCDC and +# may NOT be disclosed or redistributed in any form, either in whole or in +# part, to any third party. All copies of this code made in accordance with a +# valid licence agreement as referred to above must contain this copyright +# notice. # -# No representations, warranties, or liabilities are expressed or implied in the -# supply of this code by CCDC, its servants or agents, except where such +# No representations, warranties, or liabilities are expressed or implied in +# the supply of this code by CCDC, its servants or agents, except where such # exclusion or limitation is prohibited, void or unenforceable under governing # law. # diff --git a/main/hooks/copywrite_fix.sh b/main/hooks/copywrite_fix.sh index c1ed967..170f272 100644 --- a/main/hooks/copywrite_fix.sh +++ b/main/hooks/copywrite_fix.sh @@ -1,15 +1,16 @@ #!/usr/bin/env bash # -# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre -# (CCDC) of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. -# This code may not be used, reproduced, translated, modified, disassembled or -# copied, except in accordance with a valid licence agreement with CCDC and may -# not be disclosed or redistributed in any form, either in whole or in part, to -# any third party. All copies of this code made in accordance with a valid -# licence agreement as referred to above must contain this copyright notice. +# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre (CCDC) +# of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This +# code may not be used, reproduced, translated, modified, disassembled or +# copied, except in accordance with a valid licence agreement with CCDC and +# may NOT be disclosed or redistributed in any form, either in whole or in +# part, to any third party. All copies of this code made in accordance with a +# valid licence agreement as referred to above must contain this copyright +# notice. # -# No representations, warranties, or liabilities are expressed or implied in the -# supply of this code by CCDC, its servants or agents, except where such +# No representations, warranties, or liabilities are expressed or implied in +# the supply of this code by CCDC, its servants or agents, except where such # exclusion or limitation is prohibited, void or unenforceable under governing # law. # diff --git a/main/pre-commit.py b/main/pre-commit.py index 1e18904..5a35066 100755 --- a/main/pre-commit.py +++ b/main/pre-commit.py @@ -1,4 +1,19 @@ #!/usr/bin/env python3 +# +# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre (CCDC) +# of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This +# code may not be used, reproduced, translated, modified, disassembled or +# copied, except in accordance with a valid licence agreement with CCDC and +# may NOT be disclosed or redistributed in any form, either in whole or in +# part, to any third party. All copies of this code made in accordance with a +# valid licence agreement as referred to above must contain this copyright +# notice. +# +# No representations, warranties, or liabilities are expressed or implied in +# the supply of this code by CCDC, its servants or agents, except where such +# exclusion or limitation is prohibited, void or unenforceable under governing +# law. +# ''' This is a git hook migrated from hg. diff --git a/main/pre-merge-commit.py b/main/pre-merge-commit.py index 93c154b..8f3a8a3 100755 --- a/main/pre-merge-commit.py +++ b/main/pre-merge-commit.py @@ -1,4 +1,19 @@ #!/usr/bin/env python3 +# +# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre (CCDC) +# of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This +# code may not be used, reproduced, translated, modified, disassembled or +# copied, except in accordance with a valid licence agreement with CCDC and +# may NOT be disclosed or redistributed in any form, either in whole or in +# part, to any third party. All copies of this code made in accordance with a +# valid licence agreement as referred to above must contain this copyright +# notice. +# +# No representations, warranties, or liabilities are expressed or implied in +# the supply of this code by CCDC, its servants or agents, except where such +# exclusion or limitation is prohibited, void or unenforceable under governing +# law. +# ''' This is a git hook migrated from hg. From 8af5d3c396f3b454418e5a23685d149f2ad4f64f Mon Sep 17 00:00:00 2001 From: Manish Maharjan <112875432+mmaharjan-ccdc@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:25:09 +0100 Subject: [PATCH 05/15] SYS-8665 don't always run for copywrite_fix --- .pre-commit-hooks.yaml | 3 +-- README.md | 2 +- main/copywrite/headers/ccdc_hash.tmpl | 2 -- main/copywrite/headers/ccdc_slash.tmpl | 2 -- main/hooks/copywrite_fix.sh | 6 +++++- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index db79dd9..39d6a22 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -27,5 +27,4 @@ description: Automatically format and add copyright and license headers using Copywrite entry: main/hooks/copywrite_fix.sh language: script - pass_filenames: false - always_run: true + pass_filenames: true diff --git a/README.md b/README.md index a33864a..be63e03 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The commit will be flagged if it includes certain text files with: * Tabs * Missing terminating newline for certain files * Certain C++ #include patterns and std::exception -* Missing or non-compliant CCDC copyright and license headers (via HashiCorp Copywrite) +* Missing or non-compliant CCDC copyright and license headers when using the GitHub Action or `pre-commit` integration The commit will also be flagged if the commit message does not include a Jira ID (unless marked with NO_JIRA or a Copilot Autofix co-author line), or if the diff --git a/main/copywrite/headers/ccdc_hash.tmpl b/main/copywrite/headers/ccdc_hash.tmpl index 5adb3cd..a0b4795 100644 --- a/main/copywrite/headers/ccdc_hash.tmpl +++ b/main/copywrite/headers/ccdc_hash.tmpl @@ -1,4 +1,3 @@ -# START_LICENSE_TEXT # # This code is Copyright (C) {{ .Year }} The Cambridge Crystallographic Data Centre (CCDC) # of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This @@ -14,4 +13,3 @@ # exclusion or limitation is prohibited, void or unenforceable under governing # law. # -# END_LICENSE_TEXT \ No newline at end of file diff --git a/main/copywrite/headers/ccdc_slash.tmpl b/main/copywrite/headers/ccdc_slash.tmpl index 22357f7..7d6d3f8 100644 --- a/main/copywrite/headers/ccdc_slash.tmpl +++ b/main/copywrite/headers/ccdc_slash.tmpl @@ -1,4 +1,3 @@ -// START_LICENSE_TEXT // // This code is Copyright (C) {{ .Year }} The Cambridge Crystallographic Data Centre (CCDC) // of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This @@ -14,4 +13,3 @@ // exclusion or limitation is prohibited, void or unenforceable under governing // law. // -// END_LICENSE_TEXT \ No newline at end of file diff --git a/main/hooks/copywrite_fix.sh b/main/hooks/copywrite_fix.sh index 170f272..1f791ec 100644 --- a/main/hooks/copywrite_fix.sh +++ b/main/hooks/copywrite_fix.sh @@ -22,4 +22,8 @@ HOOK_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" CONFIG_PATH="${SCRIPT_DIR}/../copywrite/.copywrite.hcl" export COPYWRITE_HOOK_ROOT="${HOOK_ROOT}" -copywrite headers --config="${CONFIG_PATH}" +if [ $# -gt 0 ]; then + copywrite headers --config="${CONFIG_PATH}" "$@" +else + copywrite headers --config="${CONFIG_PATH}" +fi From 777389e5af4c11cc9f774013aa32da3576b51333 Mon Sep 17 00:00:00 2001 From: Manish Maharjan <112875432+mmaharjan-ccdc@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:41:42 +0100 Subject: [PATCH 06/15] SYS-8665 update action to check for changed files --- action.yml | 39 ++++++++++++++++++++++++++++++++--- main/hooks/copywrite_check.sh | 6 +++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/action.yml b/action.yml index d7b7afd..df449cb 100644 --- a/action.yml +++ b/action.yml @@ -32,9 +32,42 @@ runs: COPYWRITE_HOOK_ROOT: ${{ github.action_path }} run: | copywrite --version - copywrite headers \ - --config "$GITHUB_ACTION_PATH/main/copywrite/.copywrite.hcl" \ - --plan + + CHANGED_FILES=() + if [ "${GITHUB_EVENT_NAME:-}" = "pull_request" ] && [ -n "${GITHUB_BASE_REF:-}" ]; then + DIFF_REF="origin/${GITHUB_BASE_REF}" + if ! git rev-parse --verify "$DIFF_REF" >/dev/null 2>&1; then + DIFF_REF="remotes/origin/${GITHUB_BASE_REF}" + fi + if ! git rev-parse --verify "$DIFF_REF" >/dev/null 2>&1; then + DIFF_REF="${GITHUB_BASE_REF}" + fi + if git rev-parse --verify "$DIFF_REF" >/dev/null 2>&1; then + while IFS= read -r file; do + [ -n "$file" ] && CHANGED_FILES+=("$file") + done < <(git diff --name-only --diff-filter=d "${DIFF_REF}...HEAD") + fi + else + if git rev-parse --verify HEAD~1 >/dev/null 2>&1; then + while IFS= read -r file; do + [ -n "$file" ] && CHANGED_FILES+=("$file") + done < <(git diff --name-only --diff-filter=d HEAD~1..HEAD) + else + while IFS= read -r file; do + [ -n "$file" ] && CHANGED_FILES+=("$file") + done < <(git diff --name-only --diff-filter=d HEAD) + fi + fi + + if [ ${#CHANGED_FILES[@]} -gt 0 ]; then + echo "Checking ${#CHANGED_FILES[@]} changed file(s) for header compliance..." + copywrite headers \ + --config "$GITHUB_ACTION_PATH/main/copywrite/.copywrite.hcl" \ + --plan \ + "${CHANGED_FILES[@]}" + else + echo "No added or modified files found to check for header compliance." + fi - name: Run file compliance check shell: bash diff --git a/main/hooks/copywrite_check.sh b/main/hooks/copywrite_check.sh index 7237707..a6dd81e 100644 --- a/main/hooks/copywrite_check.sh +++ b/main/hooks/copywrite_check.sh @@ -22,4 +22,8 @@ HOOK_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" CONFIG_PATH="${SCRIPT_DIR}/../copywrite/.copywrite.hcl" export COPYWRITE_HOOK_ROOT="${HOOK_ROOT}" -copywrite headers --config="${CONFIG_PATH}" --plan +if [ $# -gt 0 ]; then + copywrite headers --config="${CONFIG_PATH}" --plan "$@" +else + copywrite headers --config="${CONFIG_PATH}" --plan +fi From 28f728a6f52786f17c360f87a2871e2f101c8386 Mon Sep 17 00:00:00 2001 From: Manish Maharjan <112875432+mmaharjan-ccdc@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:29:36 +0100 Subject: [PATCH 07/15] sys-8665 terminate option parsing before repo paths Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/action.yml b/action.yml index df449cb..fb31471 100644 --- a/action.yml +++ b/action.yml @@ -64,6 +64,7 @@ runs: copywrite headers \ --config "$GITHUB_ACTION_PATH/main/copywrite/.copywrite.hcl" \ --plan \ + -- \ "${CHANGED_FILES[@]}" else echo "No added or modified files found to check for header compliance." From 47b824a2dc7a281f3b14cfb60a62aba5dfb5f292 Mon Sep 17 00:00:00 2001 From: Manish Maharjan <112875432+mmaharjan-ccdc@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:30:54 +0100 Subject: [PATCH 08/15] SYS-8665 Add an option terminator before the filenames Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- main/hooks/copywrite_fix.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/hooks/copywrite_fix.sh b/main/hooks/copywrite_fix.sh index 1f791ec..5d19368 100644 --- a/main/hooks/copywrite_fix.sh +++ b/main/hooks/copywrite_fix.sh @@ -23,7 +23,7 @@ CONFIG_PATH="${SCRIPT_DIR}/../copywrite/.copywrite.hcl" export COPYWRITE_HOOK_ROOT="${HOOK_ROOT}" if [ $# -gt 0 ]; then - copywrite headers --config="${CONFIG_PATH}" "$@" + copywrite headers --config="${CONFIG_PATH}" -- "$@" else copywrite headers --config="${CONFIG_PATH}" fi From a7caa82229a88d53425d60050e0f5a1ab1abdf70 Mon Sep 17 00:00:00 2001 From: Manish Maharjan <112875432+mmaharjan-ccdc@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:21:18 +0100 Subject: [PATCH 09/15] SYS-8665 update readme for github actions --- README.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index be63e03..a032788 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ -This repository contains files that can be used as a github action and local -git hooks. +This repository provides code quality and compliance tooling that serves three purposes: -It does a few checks on source code to ensure compliance with some general -CCDC coding standard. +1. **Native Git Hooks** (`main/`): Local hooks for standard Git workflow (`commit-msg`, `pre-commit`, `pre-merge-commit`). +2. **`pre-commit` Integration** (`.pre-commit-hooks.yaml`): Hooks for the [`pre-commit`](https://pre-commit.com/) framework (`copywrite-fix`, `copywrite-check`). +3. **GitHub Action** (`action.yml`): Composite GitHub Action for CI workflows to validate copyright headers and repository compliance on PRs/commits. + +It does a few checks on source code to ensure compliance with general CCDC coding standards. The commit will be flagged if it includes certain text files with: @@ -19,13 +21,17 @@ ID (unless marked with NO_JIRA or a Copilot Autofix co-author line), or if the size of new or modified files exceeds a threshold. -# Github action +# GitHub Actions + +This repository provides a composite GitHub Action for validating copyright +headers and file compliance rules in CI. ## Usage + ```yaml - uses: ccdc-opensource/commit-hooks@v7 with: - commitMessage: 'The commit message' + commitMessage: ${{ github.event.head_commit.message }} ``` ## Scenarios @@ -43,14 +49,14 @@ jobs: with: ref: ${{ github.head_ref }} fetch-depth: 0 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v7 with: python-version: "3.11" - name: Get the commit message run: | echo "commit_message=$(git log --format=%B -n 1 ${{ github.event.after }})" >> $GITHUB_ENV shell: bash - - uses: ccdc-opensource/commit-hooks@v7 + - uses: ccdc-opensource/commit-hooks@v8 with: commitMessage: ${{ env.commit_message }} ``` From c98566ec73f8c57c84647f269f694592a59cd624 Mon Sep 17 00:00:00 2001 From: Manish Maharjan <112875432+mmaharjan-ccdc@users.noreply.github.com> Date: Fri, 28 Aug 2026 01:47:00 +0100 Subject: [PATCH 10/15] SYS-8665 insert how to get copywrite on windows --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index a032788..2b39f05 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,12 @@ repos: The `copywrite-check` and `copywrite-fix` hooks require the `copywrite` CLI to be available on your `PATH`. +**Windows (Chocolatey)** + +```powershell +choco install copywrite +``` + **macOS / Linux (Homebrew)** ```bash From 1c77ab355e637ef4db8cf7750b0e752282c00483 Mon Sep 17 00:00:00 2001 From: Manish Maharjan <112875432+mmaharjan-ccdc@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:55:09 +0100 Subject: [PATCH 11/15] SYS-8665 create template files --- README.md | 41 ++++++++++++++++++++++--------- main/copywrite/.copywrite.hcl | 1 + templates/.pre-commit-config.yaml | 11 +++++++++ templates/compliance.yml | 33 +++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 templates/.pre-commit-config.yaml create mode 100644 templates/compliance.yml diff --git a/README.md b/README.md index 2b39f05..8789bca 100644 --- a/README.md +++ b/README.md @@ -29,34 +29,49 @@ headers and file compliance rules in CI. ## Usage ```yaml -- uses: ccdc-opensource/commit-hooks@v7 +- uses: ccdc-opensource/commit-hooks@v8 with: commitMessage: ${{ github.event.head_commit.message }} ``` -## Scenarios -### Check files in pull request for merge to main +## Workflow Template + +A ready to use template is available in [`templates/compliance.yml`](templates/compliance.yml). + +You can copy this file directly into your repository at `.github/workflows/compliance.yml`, or distribute it across your organisation via your centralised GitHub repository management system. + ```yaml -name: Check pull request files +name: Header & Compliance Checks + on: - pull_request - branches: [ main ] + pull_request: + branches: [main] + push: + branches: [main ] + jobs: - Pull-request-files-check: + compliance-check: + name: Validate Headers & Code Compliance runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - name: Checkout repository + uses: actions/checkout@v7 with: ref: ${{ github.head_ref }} fetch-depth: 0 - - uses: actions/setup-python@v7 + + - name: Set up Python + uses: actions/setup-python@v7 with: python-version: "3.11" - - name: Get the commit message + + - name: Extract commit message run: | - echo "commit_message=$(git log --format=%B -n 1 ${{ github.event.after }})" >> $GITHUB_ENV + echo "commit_message=$(git log --format=%B -n 1 ${{ github.event.after || github.sha }})" >> $GITHUB_ENV shell: bash - - uses: ccdc-opensource/commit-hooks@v8 + + - name: Run CCDC Commit Hooks & Header Checks + uses: ccdc-opensource/commit-hooks@v8 with: commitMessage: ${{ env.commit_message }} ``` @@ -99,6 +114,8 @@ headers using [HashiCorp Copywrite](https://github.com/hashicorp/copywrite). ### Example Configuration +A starter config template is available in [`templates/.pre-commit-config.yaml`](templates/.pre-commit-config.yaml). + Add the following to your `.pre-commit-config.yaml`: ```yaml diff --git a/main/copywrite/.copywrite.hcl b/main/copywrite/.copywrite.hcl index 5c415e9..c76c316 100644 --- a/main/copywrite/.copywrite.hcl +++ b/main/copywrite/.copywrite.hcl @@ -8,6 +8,7 @@ project { ".github/**", "test/**", "tests/**", + "templates/**", "**/bin/**", "**/obj/**", "**/packages/**", diff --git a/templates/.pre-commit-config.yaml b/templates/.pre-commit-config.yaml new file mode 100644 index 0000000..5264bce --- /dev/null +++ b/templates/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +# Standard CCDC pre-commit configuration +# Distribute this file to repositories to enable automatic copyright header formatting +repos: + - repo: https://github.com/ccdc-opensource/commit-hooks + rev: v8 + hooks: + # Recommended: Automatically inserts or updates CCDC copyright headers on commit + - id: copywrite-fix + + # Alternative: Validates compliance without modifying files (e.g. for local lint checks) + # - id: copywrite-check diff --git a/templates/compliance.yml b/templates/compliance.yml new file mode 100644 index 0000000..e1f4284 --- /dev/null +++ b/templates/compliance.yml @@ -0,0 +1,33 @@ +name: Header & Compliance Checks + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + compliance-check: + name: Validate Headers & Code Compliance + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: "3.11" + + - name: Extract commit message + shell: bash + run: | + echo "commit_message=$(git log --format=%B -n 1 ${{ github.event.after || github.sha }})" >> $GITHUB_ENV + + - name: Run CCDC Commit Hooks & Header Checks + uses: ccdc-opensource/commit-hooks@v8 + with: + commitMessage: ${{ env.commit_message }} From 6d90dd0e39ae6c83697924a84de2f74f265cc7b9 Mon Sep 17 00:00:00 2001 From: Manish Maharjan <112875432+mmaharjan-ccdc@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:33:02 +0100 Subject: [PATCH 12/15] SYS-8665 integrate into githooks.py as optional, update readme and remove old templates --- .pre-commit-hooks.yaml | 30 ------ README.md | 168 ++++++------------------------ main/githooks.py | 80 +++++++++++++- templates/.pre-commit-config.yaml | 11 -- 4 files changed, 110 insertions(+), 179 deletions(-) delete mode 100644 .pre-commit-hooks.yaml delete mode 100644 templates/.pre-commit-config.yaml diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml deleted file mode 100644 index 39d6a22..0000000 --- a/.pre-commit-hooks.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# -# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre (CCDC) -# of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This -# code may not be used, reproduced, translated, modified, disassembled or -# copied, except in accordance with a valid licence agreement with CCDC and -# may NOT be disclosed or redistributed in any form, either in whole or in -# part, to any third party. All copies of this code made in accordance with a -# valid licence agreement as referred to above must contain this copyright -# notice. -# -# No representations, warranties, or liabilities are expressed or implied in -# the supply of this code by CCDC, its servants or agents, except where such -# exclusion or limitation is prohibited, void or unenforceable under governing -# law. -# - -- id: copywrite-check - name: copywrite-check - description: Validate copyright and license header compliance using Copywrite - entry: main/hooks/copywrite_check.sh - language: script - pass_filenames: false - always_run: true - -- id: copywrite-fix - name: copywrite-fix - description: Automatically format and add copyright and license headers using Copywrite - entry: main/hooks/copywrite_fix.sh - language: script - pass_filenames: true diff --git a/README.md b/README.md index 8789bca..27db324 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ -This repository provides code quality and compliance tooling that serves three purposes: +This repository provides code quality and compliance tooling for CCDC repositories: -1. **Native Git Hooks** (`main/`): Local hooks for standard Git workflow (`commit-msg`, `pre-commit`, `pre-merge-commit`). -2. **`pre-commit` Integration** (`.pre-commit-hooks.yaml`): Hooks for the [`pre-commit`](https://pre-commit.com/) framework (`copywrite-fix`, `copywrite-check`). -3. **GitHub Action** (`action.yml`): Composite GitHub Action for CI workflows to validate copyright headers and repository compliance on PRs/commits. +1. **Native Git Hooks** (`main/`): Local hooks configured globally (`commit-msg`, `pre-commit`, `pre-merge-commit`) for standard Git workflows. +2. **GitHub Action** (`action.yml`): Composite GitHub Action for CI workflows to validate copyright headers and repository compliance on PRs/commits. It does a few checks on source code to ensure compliance with general CCDC coding standards. @@ -14,7 +13,7 @@ The commit will be flagged if it includes certain text files with: * Tabs * Missing terminating newline for certain files * Certain C++ #include patterns and std::exception -* Missing or non-compliant CCDC copyright and license headers when using the GitHub Action or `pre-commit` integration +* Missing or non-compliant CCDC copyright and license headers (when using the GitHub Action or local copywrite integration) The commit will also be flagged if the commit message does not include a Jira ID (unless marked with NO_JIRA or a Copilot Autofix co-author line), or if the @@ -34,140 +33,39 @@ headers and file compliance rules in CI. commitMessage: ${{ github.event.head_commit.message }} ``` -## Workflow Template +A complete workflow template for CI is available in [templates/compliance.yml](templates/compliance.yml). -A ready to use template is available in [`templates/compliance.yml`](templates/compliance.yml). +# Native Git Hooks -You can copy this file directly into your repository at `.github/workflows/compliance.yml`, or distribute it across your organisation via your centralised GitHub repository management system. - -```yaml -name: Header & Compliance Checks - -on: - pull_request: - branches: [main] - push: - branches: [main ] - -jobs: - compliance-check: - name: Validate Headers & Code Compliance - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v7 - with: - ref: ${{ github.head_ref }} - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v7 - with: - python-version: "3.11" - - - name: Extract commit message - run: | - echo "commit_message=$(git log --format=%B -n 1 ${{ github.event.after || github.sha }})" >> $GITHUB_ENV - shell: bash - - - name: Run CCDC Commit Hooks & Header Checks - uses: ccdc-opensource/commit-hooks@v8 - with: - commitMessage: ${{ env.commit_message }} -``` - -# commit-hooks -You can use this as git hooks for local repositories. - -A set of hooks include: -* commit-msg -* pre-commit -* pre-merge-commit - -## Setting up Core Git Hooks +To enable CCDC commit checks (Jira ID, CRLF, line endings, DO NOT COMMIT, file size, and automatic copyright headers) globally for all repositories on your machine: 1. Clone this repository. -2. Configure Git to use the hooks: - -```bash -git config --global core.hooksPath /main -``` - -This enables the CCDC commit hooks for all repositories on your machine. - -## Using with `pre-commit` - -This repository also provides hooks compatible with the -[`pre-commit`](https://pre-commit.com/) framework for managing copyright -headers using [HashiCorp Copywrite](https://github.com/hashicorp/copywrite). - -### Available Hooks - -* **`copywrite-fix`** *(recommended for local development)*: - Automatically inserts or updates the CCDC copyright and licence headers - in newly added or modified files. - -* **`copywrite-check`**: - Validates copyright and licence headers across tracked files and fails if - any files are non-compliant. This hook is well suited for CI/CD pipelines - and verification workflows. - -### Example Configuration - -A starter config template is available in [`templates/.pre-commit-config.yaml`](templates/.pre-commit-config.yaml). - -Add the following to your `.pre-commit-config.yaml`: - -```yaml -repos: - - repo: https://github.com/ccdc-opensource/commit-hooks - rev: - hooks: - # Automatically inserts or updates headers - - id: copywrite-fix - - # Optional: validates compliance after formatting - # - id: copywrite-check -``` - -### Copywrite Prerequisite - -The `copywrite-check` and `copywrite-fix` hooks require the `copywrite` -CLI to be available on your `PATH`. - -**Windows (Chocolatey)** - -```powershell -choco install copywrite -``` - -**macOS / Linux (Homebrew)** - -```bash -brew install hashicorp/tap/copywrite -``` - -**Go** - -```bash -go install github.com/hashicorp/copywrite@latest -``` - -**Direct download** - -Binary releases are available from: - -[HashiCorp Copywrite Releases](https://github.com/hashicorp/copywrite/releases) - -### Recommended Usage - -For the best developer experience: - -* Use **`copywrite-fix`** locally to automatically insert or update headers. -* Use **`copywrite-check`** in CI/CD pipelines to enforce compliance. - -This ensures that copyright and licence headers are automatically maintained -while also preventing non-compliant changes from being merged. +2. Run: + ```bash + git config --global core.hooksPath /main + ``` +3. (Optional) Install `copywrite` to automatically add and format CCDC copyright headers on commit: + * **Windows:** `choco install copywrite` + * **macOS:** `brew install hashicorp/tap/copywrite` + * **Linux:** `go install github.com/hashicorp/copywrite@latest` + +> **Note:** If `copywrite` is not installed on your machine, native hooks will continue to run all other standard checks and display a gentle warning without failing your commit. + +## Configuring Copywrite Behavior + +Developers can customise the copywrite hook using Git configuration: + +* **Enable / Disable Copywrite:** + ```bash + git config --global hooks.copywrite true # opt-in: enable copywrite integration + git config --global hooks.copywrite false # default: disabled + ``` + +* **Set Mode (`fix` vs `check`):** + ```bash + git config --global hooks.copywriteMode fix # default: automatically inserts/updates headers on commit + git config --global hooks.copywriteMode check # read-only check (warns/fails if headers are missing) + ``` ## Recommended settings ### To ensure the line endings are correctly converted: diff --git a/main/githooks.py b/main/githooks.py index 08595e9..dc8af6e 100644 --- a/main/githooks.py +++ b/main/githooks.py @@ -27,6 +27,7 @@ import os import platform import re +import shutil import subprocess import unittest import sys @@ -982,6 +983,74 @@ def _test(input, is_good=True): _test('Close but no cigar abc-1234', False) +def run_copywrite(files): + '''Run copywrite to automatically check or fix license headers. + + Configurable via git config: + - `git config --global hooks.copywrite true|false` (default: false, opt-in) + - `git config --global hooks.copywriteMode fix|check` (default: fix) + - fix: automatically adds/updates headers and restages files + - check: checks header compliance and warns/fails without modifying + + If copywrite is not installed and the hook is enabled, print a soft warning and return 0 (do not block commit). + ''' + if not files: + return 0 + + # Opt-in: only run if explicitly enabled in git config + enabled_setting = get_config_setting('hooks.copywrite') + if enabled_setting is None or enabled_setting.lower() not in ['true', '1', 'yes', 'on']: + return 0 + + copywrite_exe = shutil.which('copywrite') + if not copywrite_exe: + print(' WARNING: "copywrite" not found on PATH. Skipping copyright header check.') + print(' To enable automatic copyright formatting, install copywrite:') + print(' - Windows: choco install copywrite') + print(' - macOS: brew install hashicorp/tap/copywrite') + print(' - Linux: go install github.com/hashicorp/copywrite@latest') + return 0 + + hook_root = Path(__file__).resolve().parent.parent + config_path = hook_root / 'main' / 'copywrite' / '.copywrite.hcl' + if not config_path.is_file(): + return 0 + + env = os.environ.copy() + env['COPYWRITE_HOOK_ROOT'] = str(hook_root) + + mode_setting = get_config_setting('hooks.copywriteMode') + is_check_mode = mode_setting is not None and mode_setting.lower() in ['check', 'plan', 'verify'] + + cmd = [copywrite_exe, 'headers', f'--config={config_path}'] + if is_check_mode: + cmd.append('--plan') + cmd.extend(files) + + try: + proc = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=env + ) + if proc.returncode != 0: + if is_check_mode: + _fail(f'Copyright header check failed:\n{proc.stdout or proc.stderr}') + return 1 + else: + print(f' Copywrite warning:\n{proc.stderr.strip()}') + else: + if not is_check_mode: + # Re-stage any files that copywrite updated + subprocess.run(['git', 'add'] + files, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except Exception as e: + print(f' WARNING: Failed to run copywrite: {e}') + + return 0 + + def commit_hook(merge=False): retval = 0 files = get_commit_files() @@ -994,14 +1063,19 @@ def commit_hook(merge=False): retval += check_do_not_merge(files['M']) retval += check_do_not_merge(files['A'], new_files=True) else: + staged_files = files['M'] + files['A'] + + print(' Check and update copyright headers ...') + retval += run_copywrite(staged_files) + print(' Check filenames ...') - retval += check_filenames(files['M'] + files['A']) + retval += check_filenames(staged_files) print(' Check line endings ...') - retval += check_eol(files['M'] + files['A']) + retval += check_eol(staged_files) print(' Check file content ...') - retval += check_content(files['M'] + files['A']) + retval += check_content(staged_files) return retval diff --git a/templates/.pre-commit-config.yaml b/templates/.pre-commit-config.yaml deleted file mode 100644 index 5264bce..0000000 --- a/templates/.pre-commit-config.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Standard CCDC pre-commit configuration -# Distribute this file to repositories to enable automatic copyright header formatting -repos: - - repo: https://github.com/ccdc-opensource/commit-hooks - rev: v8 - hooks: - # Recommended: Automatically inserts or updates CCDC copyright headers on commit - - id: copywrite-fix - - # Alternative: Validates compliance without modifying files (e.g. for local lint checks) - # - id: copywrite-check From 1990b3bde6174455502a99c8e9174df00ff637cf Mon Sep 17 00:00:00 2001 From: Manish Maharjan <112875432+mmaharjan-ccdc@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:15:36 +0100 Subject: [PATCH 13/15] SYS-8665 co-pilot comments --- .github/workflows/status_check.yml | 3 +- README.md | 13 ++- action.yml | 38 +++++++-- main/githooks.py | 133 +++++++++++++++++++++++++---- main/hooks/copywrite_check.sh | 29 ------- main/hooks/copywrite_fix.sh | 29 ------- templates/compliance.yml | 7 +- 7 files changed, 165 insertions(+), 87 deletions(-) delete mode 100644 main/hooks/copywrite_check.sh delete mode 100644 main/hooks/copywrite_fix.sh diff --git a/.github/workflows/status_check.yml b/.github/workflows/status_check.yml index 9d85ecc..5b2c771 100644 --- a/.github/workflows/status_check.yml +++ b/.github/workflows/status_check.yml @@ -17,9 +17,10 @@ jobs: - name: Get the commit message run: | echo 'commit_message<> $GITHUB_ENV - git log --format=%B -n 1 ${{ github.event.after }} >> $GITHUB_ENV + git log --format=%B -n 1 HEAD >> $GITHUB_ENV echo 'EOF' >> $GITHUB_ENV shell: bash - uses: ccdc-opensource/commit-hooks@main with: commitMessage: ${{ env.commit_message }} + licenseCheck: true diff --git a/README.md b/README.md index 27db324..8568f7d 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,18 @@ headers and file compliance rules in CI. ## Usage ```yaml -- uses: ccdc-opensource/commit-hooks@v8 +- name: Extract commit message + shell: bash + run: | + echo 'commit_message<> "$GITHUB_ENV" + git log --format=%B -n 1 HEAD >> "$GITHUB_ENV" + echo 'EOF' >> "$GITHUB_ENV" + +- uses: ccdc-opensource/commit-hooks@main with: - commitMessage: ${{ github.event.head_commit.message }} + commitMessage: ${{ env.commit_message }} + # Optional: enable CCDC license header validation on PR changed files + licenseCheck: true # default: false (opt-in) ``` A complete workflow template for CI is available in [templates/compliance.yml](templates/compliance.yml). diff --git a/action.yml b/action.yml index fb31471..095af22 100644 --- a/action.yml +++ b/action.yml @@ -20,16 +20,23 @@ inputs: commitMessage: description: 'The commit message' required: true + licenseCheck: + description: 'Validate CCDC copyright and license headers on changed files (true/false)' + required: false + default: 'false' runs: using: composite steps: - name: Install copywrite + if: ${{ inputs.licenseCheck == 'true' }} uses: hashicorp/setup-copywrite@v1.1.3 - name: Validate Header Compliance + if: ${{ inputs.licenseCheck == 'true' }} shell: bash env: COPYWRITE_HOOK_ROOT: ${{ github.action_path }} + GITHUB_EVENT_BEFORE: ${{ github.event.before }} run: | copywrite --version @@ -42,21 +49,35 @@ runs: if ! git rev-parse --verify "$DIFF_REF" >/dev/null 2>&1; then DIFF_REF="${GITHUB_BASE_REF}" fi - if git rev-parse --verify "$DIFF_REF" >/dev/null 2>&1; then - while IFS= read -r file; do - [ -n "$file" ] && CHANGED_FILES+=("$file") - done < <(git diff --name-only --diff-filter=d "${DIFF_REF}...HEAD") + if ! git rev-parse --verify "$DIFF_REF" >/dev/null 2>&1; then + echo "Unable to resolve pull request base ref: ${GITHUB_BASE_REF}" >&2 + exit 1 fi - else - if git rev-parse --verify HEAD~1 >/dev/null 2>&1; then + while IFS= read -r file; do + [ -n "$file" ] && CHANGED_FILES+=("$file") + done < <(git diff --name-only --diff-filter=d "${DIFF_REF}...HEAD") + elif [ "${GITHUB_EVENT_NAME:-}" = "push" ]; then + if [[ "${GITHUB_EVENT_BEFORE:-}" =~ ^0+$ ]]; then while IFS= read -r file; do [ -n "$file" ] && CHANGED_FILES+=("$file") - done < <(git diff --name-only --diff-filter=d HEAD~1..HEAD) + done < <(git diff-tree --root --no-commit-id --name-only --diff-filter=d -r HEAD) else + if [ -z "${GITHUB_EVENT_BEFORE:-}" ] || ! git rev-parse --verify "${GITHUB_EVENT_BEFORE}^{commit}" >/dev/null 2>&1; then + echo "Unable to resolve push baseline: ${GITHUB_EVENT_BEFORE:-}" >&2 + exit 1 + fi while IFS= read -r file; do [ -n "$file" ] && CHANGED_FILES+=("$file") - done < <(git diff --name-only --diff-filter=d HEAD) + done < <(git diff --name-only --diff-filter=d "${GITHUB_EVENT_BEFORE}..HEAD") fi + elif git rev-parse --verify HEAD^ >/dev/null 2>&1; then + while IFS= read -r file; do + [ -n "$file" ] && CHANGED_FILES+=("$file") + done < <(git diff --name-only --diff-filter=d HEAD^..HEAD) + else + while IFS= read -r file; do + [ -n "$file" ] && CHANGED_FILES+=("$file") + done < <(git diff-tree --root --no-commit-id --name-only --diff-filter=d -r HEAD) fi if [ ${#CHANGED_FILES[@]} -gt 0 ]; then @@ -74,6 +95,7 @@ runs: shell: bash env: INPUT_COMMITMESSAGE: ${{ inputs.commitMessage }} + GITHUB_EVENT_BEFORE: ${{ github.event.before }} run: | python3 "$GITHUB_ACTION_PATH/main.py" diff --git a/main/githooks.py b/main/githooks.py index dc8af6e..79f040c 100644 --- a/main/githooks.py +++ b/main/githooks.py @@ -208,7 +208,13 @@ def get_commit_files(): if _is_pull_request(): commands += [f'remotes/origin/{os.environ["GITHUB_BASE_REF"]}..remotes/origin/{os.environ["GITHUB_HEAD_REF"]}','--'] else: - commands += ['HEAD~..', '--'] + before_sha = os.environ.get('GITHUB_EVENT_BEFORE', '') + if before_sha and set(before_sha) == {'0'}: + commands = ['git', 'diff-tree', '--root', '--no-commit-id', '--name-status', '-r', 'HEAD', '--'] + elif before_sha: + commands += [f'{before_sha}..HEAD', '--'] + else: + commands += ['HEAD~..', '--'] else: commands = ['git', 'diff-index', '--ignore-submodules', 'HEAD', '--cached'] @@ -275,9 +281,15 @@ def get_changed_lines(modified_file): if _is_pull_request(): commands += [f'remotes/origin/{os.environ["GITHUB_BASE_REF"]}..remotes/origin/{os.environ["GITHUB_HEAD_REF"]}', '--',f'{modified_file}'] else: - commands += ['HEAD~', f'{modified_file}'] + before_sha = os.environ.get('GITHUB_EVENT_BEFORE', '') + if before_sha and set(before_sha) == {'0'}: + commands = ['git', 'diff-tree', '--root', '--no-commit-id', '--unified=0', '-r', 'HEAD', '--', modified_file] + elif before_sha: + commands += [f'{before_sha}..HEAD', '--', modified_file] + else: + commands += ['HEAD~', '--', modified_file] else: - commands = [f'git', 'diff-index', 'HEAD', '--unified=0', f'{modified_file}'] + commands = [f'git', 'diff-index', 'HEAD', '--unified=0', '--', f'{modified_file}'] output = _get_output(commands) lines = [] @@ -1014,20 +1026,40 @@ def run_copywrite(files): hook_root = Path(__file__).resolve().parent.parent config_path = hook_root / 'main' / 'copywrite' / '.copywrite.hcl' if not config_path.is_file(): - return 0 + _fail(f'Copywrite configuration not found: {config_path}') + return 1 env = os.environ.copy() env['COPYWRITE_HOOK_ROOT'] = str(hook_root) - mode_setting = get_config_setting('hooks.copywriteMode') - is_check_mode = mode_setting is not None and mode_setting.lower() in ['check', 'plan', 'verify'] + mode = (get_config_setting('hooks.copywriteMode') or 'fix').lower() + if mode not in ['fix', 'check', 'plan', 'verify']: + _fail(f'Unsupported hooks.copywriteMode value: {mode}') + return 1 + is_check_mode = mode in ['check', 'plan', 'verify'] cmd = [copywrite_exe, 'headers', f'--config={config_path}'] if is_check_mode: cmd.append('--plan') + cmd.append('--') cmd.extend(files) try: + if not is_check_mode: + unstaged = subprocess.run( + ['git', 'diff', '--quiet', '--'] + files, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True + ) + if unstaged.returncode == 1: + _fail('Copywrite fix mode cannot run with unstaged changes in staged files. ' + 'Stage or stash those changes, or use hooks.copywriteMode check.') + return 1 + if unstaged.returncode != 0: + _fail(f'Unable to inspect unstaged changes:\n{unstaged.stderr.strip()}') + return 1 + proc = subprocess.run( cmd, stdout=subprocess.PIPE, @@ -1036,21 +1068,90 @@ def run_copywrite(files): env=env ) if proc.returncode != 0: - if is_check_mode: - _fail(f'Copyright header check failed:\n{proc.stdout or proc.stderr}') + details = '\n'.join(output.strip() for output in [proc.stdout, proc.stderr] if output.strip()) + _fail(f'Copyright header update failed:\n{details}') + return 1 + if not is_check_mode: + restage = subprocess.run( + ['git', 'add', '--'] + files, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True + ) + if restage.returncode != 0: + _fail(f'Unable to restage files updated by Copywrite:\n{restage.stderr.strip()}') return 1 - else: - print(f' Copywrite warning:\n{proc.stderr.strip()}') - else: - if not is_check_mode: - # Re-stage any files that copywrite updated - subprocess.run(['git', 'add'] + files, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except Exception as e: - print(f' WARNING: Failed to run copywrite: {e}') + except (OSError, subprocess.SubprocessError) as error: + _fail(f'Failed to run Copywrite: {error}') + return 1 return 0 +class TestRunCopywrite(unittest.TestCase): + @patch('githooks.get_config_setting', return_value=None) + def test_disabled(self, _config): + self.assertEqual(0, run_copywrite(['example.py'])) + + @patch('githooks.shutil.which', return_value=None) + @patch('githooks.get_config_setting', return_value='true') + def test_missing_executable_is_soft_failure(self, _config, _which): + self.assertEqual(0, run_copywrite(['example.py'])) + + @patch('githooks.Path.is_file', return_value=True) + @patch('githooks.shutil.which', return_value='copywrite') + @patch('githooks.get_config_setting', side_effect=['true', 'check']) + @patch('githooks.subprocess.run') + def test_check_failure_blocks_commit(self, run, _config, _which, _is_file): + run.return_value = subprocess.CompletedProcess([], 1, 'stdout', 'stderr') + self.assertEqual(1, run_copywrite(['example.py'])) + + @patch('githooks.Path.is_file', return_value=True) + @patch('githooks.shutil.which', return_value='copywrite') + @patch('githooks.get_config_setting', side_effect=['true', 'fix']) + @patch('githooks.subprocess.run') + def test_fix_restages_files(self, run, _config, _which, _is_file): + run.side_effect = [ + subprocess.CompletedProcess([], 0, '', ''), + subprocess.CompletedProcess([], 0, '', ''), + subprocess.CompletedProcess([], 0, '', ''), + ] + self.assertEqual(0, run_copywrite(['example.py'])) + self.assertEqual( + ['copywrite', 'headers', unittest.mock.ANY, '--', 'example.py'], + run.call_args_list[1].args[0] + ) + self.assertEqual(['git', 'add', '--', 'example.py'], run.call_args_list[-1].args[0]) + + @patch('githooks.Path.is_file', return_value=True) + @patch('githooks.shutil.which', return_value='copywrite') + @patch('githooks.get_config_setting', side_effect=['true', 'fix']) + @patch('githooks.subprocess.run') + def test_restage_failure_blocks_commit(self, run, _config, _which, _is_file): + run.side_effect = [ + subprocess.CompletedProcess([], 0, '', ''), + subprocess.CompletedProcess([], 0, '', ''), + subprocess.CompletedProcess([], 1, '', 'cannot add'), + ] + self.assertEqual(1, run_copywrite(['example.py'])) + + @patch('githooks.Path.is_file', return_value=True) + @patch('githooks.shutil.which', return_value='copywrite') + @patch('githooks.get_config_setting', side_effect=['true', 'fix']) + @patch('githooks.subprocess.run') + def test_fix_rejects_partially_staged_files(self, run, _config, _which, _is_file): + run.return_value = subprocess.CompletedProcess([], 1, '', '') + self.assertEqual(1, run_copywrite(['example.py'])) + run.assert_called_once() + + @patch('githooks.Path.is_file', return_value=True) + @patch('githooks.shutil.which', return_value='copywrite') + @patch('githooks.get_config_setting', side_effect=['true', 'check']) + @patch('githooks.subprocess.run', side_effect=OSError('cannot execute')) + def test_subprocess_error_blocks_commit(self, _run, _config, _which, _is_file): + self.assertEqual(1, run_copywrite(['example.py'])) + + def commit_hook(merge=False): retval = 0 files = get_commit_files() diff --git a/main/hooks/copywrite_check.sh b/main/hooks/copywrite_check.sh deleted file mode 100644 index a6dd81e..0000000 --- a/main/hooks/copywrite_check.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# -# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre (CCDC) -# of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This -# code may not be used, reproduced, translated, modified, disassembled or -# copied, except in accordance with a valid licence agreement with CCDC and -# may NOT be disclosed or redistributed in any form, either in whole or in -# part, to any third party. All copies of this code made in accordance with a -# valid licence agreement as referred to above must contain this copyright -# notice. -# -# No representations, warranties, or liabilities are expressed or implied in -# the supply of this code by CCDC, its servants or agents, except where such -# exclusion or limitation is prohibited, void or unenforceable under governing -# law. -# -set -euo pipefail -command -v copywrite >/dev/null 2>&1 || { echo "copywrite not found on PATH"; exit 1; } - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -HOOK_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" -CONFIG_PATH="${SCRIPT_DIR}/../copywrite/.copywrite.hcl" - -export COPYWRITE_HOOK_ROOT="${HOOK_ROOT}" -if [ $# -gt 0 ]; then - copywrite headers --config="${CONFIG_PATH}" --plan "$@" -else - copywrite headers --config="${CONFIG_PATH}" --plan -fi diff --git a/main/hooks/copywrite_fix.sh b/main/hooks/copywrite_fix.sh deleted file mode 100644 index 5d19368..0000000 --- a/main/hooks/copywrite_fix.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# -# This code is Copyright (C) 2026 The Cambridge Crystallographic Data Centre (CCDC) -# of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC. This -# code may not be used, reproduced, translated, modified, disassembled or -# copied, except in accordance with a valid licence agreement with CCDC and -# may NOT be disclosed or redistributed in any form, either in whole or in -# part, to any third party. All copies of this code made in accordance with a -# valid licence agreement as referred to above must contain this copyright -# notice. -# -# No representations, warranties, or liabilities are expressed or implied in -# the supply of this code by CCDC, its servants or agents, except where such -# exclusion or limitation is prohibited, void or unenforceable under governing -# law. -# -set -euo pipefail -command -v copywrite >/dev/null 2>&1 || { echo "copywrite not found on PATH"; exit 1; } - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -HOOK_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" -CONFIG_PATH="${SCRIPT_DIR}/../copywrite/.copywrite.hcl" - -export COPYWRITE_HOOK_ROOT="${HOOK_ROOT}" -if [ $# -gt 0 ]; then - copywrite headers --config="${CONFIG_PATH}" -- "$@" -else - copywrite headers --config="${CONFIG_PATH}" -fi diff --git a/templates/compliance.yml b/templates/compliance.yml index e1f4284..eedd405 100644 --- a/templates/compliance.yml +++ b/templates/compliance.yml @@ -25,9 +25,12 @@ jobs: - name: Extract commit message shell: bash run: | - echo "commit_message=$(git log --format=%B -n 1 ${{ github.event.after || github.sha }})" >> $GITHUB_ENV + echo 'commit_message<> "$GITHUB_ENV" + git log --format=%B -n 1 HEAD >> "$GITHUB_ENV" + echo 'EOF' >> "$GITHUB_ENV" - name: Run CCDC Commit Hooks & Header Checks - uses: ccdc-opensource/commit-hooks@v8 + uses: ccdc-opensource/commit-hooks@main with: commitMessage: ${{ env.commit_message }} + licenseCheck: true From 481fd2f6769585abbd06ee0dbc23d0b98e26a107 Mon Sep 17 00:00:00 2001 From: Manish Maharjan <112875432+mmaharjan-ccdc@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:42:23 +0100 Subject: [PATCH 14/15] SYS-8665 use v8 branch and add unit tests --- .github/workflows/quality_check.yml | 2 +- .github/workflows/status_check.yml | 13 ++++++---- README.md | 24 ++++++++++++++---- main/githooks.py | 38 +++++++++++++++++++++++++++++ templates/compliance.yml | 11 ++++++--- 5 files changed, 73 insertions(+), 15 deletions(-) diff --git a/.github/workflows/quality_check.yml b/.github/workflows/quality_check.yml index 0238c7f..368b331 100644 --- a/.github/workflows/quality_check.yml +++ b/.github/workflows/quality_check.yml @@ -4,7 +4,7 @@ jobs: quality-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-python@v6 with: python-version: "3.11" diff --git a/.github/workflows/status_check.yml b/.github/workflows/status_check.yml index 5b2c771..0ff6811 100644 --- a/.github/workflows/status_check.yml +++ b/.github/workflows/status_check.yml @@ -7,7 +7,7 @@ jobs: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ github.head_ref }} fetch-depth: 0 @@ -16,11 +16,14 @@ jobs: python-version: "3.11" - name: Get the commit message run: | - echo 'commit_message<> $GITHUB_ENV - git log --format=%B -n 1 HEAD >> $GITHUB_ENV - echo 'EOF' >> $GITHUB_ENV + delimiter="$(python -c 'import uuid; print(uuid.uuid4())')" + { + echo "commit_message<<${delimiter}" + git log --format=%B -n 1 HEAD + echo "${delimiter}" + } >> "$GITHUB_ENV" shell: bash - - uses: ccdc-opensource/commit-hooks@main + - uses: ./ with: commitMessage: ${{ env.commit_message }} licenseCheck: true diff --git a/README.md b/README.md index 8568f7d..cc73bad 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,28 @@ headers and file compliance rules in CI. ## Usage ```yaml +- name: Checkout repository + uses: actions/checkout@v7 + with: + ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }} + fetch-depth: 0 + +- name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: "3.11" + - name: Extract commit message shell: bash run: | - echo 'commit_message<> "$GITHUB_ENV" - git log --format=%B -n 1 HEAD >> "$GITHUB_ENV" - echo 'EOF' >> "$GITHUB_ENV" - -- uses: ccdc-opensource/commit-hooks@main + delimiter="$(python -c 'import uuid; print(uuid.uuid4())')" + { + echo "commit_message<<${delimiter}" + git log --format=%B -n 1 HEAD + echo "${delimiter}" + } >> "$GITHUB_ENV" + +- uses: ccdc-opensource/commit-hooks@v8 with: commitMessage: ${{ env.commit_message }} # Optional: enable CCDC license header validation on PR changed files diff --git a/main/githooks.py b/main/githooks.py index 79f040c..839e24f 100644 --- a/main/githooks.py +++ b/main/githooks.py @@ -300,6 +300,44 @@ def get_changed_lines(modified_file): return lines +class TestPushRanges(unittest.TestCase): + def _assert_ranges(self, before_sha, expected_files_command, expected_lines_command): + environment = {'GITHUB_EVENT_NAME': 'push'} + if before_sha is not None: + environment['GITHUB_EVENT_BEFORE'] = before_sha + + with patch.dict(os.environ, environment, clear=True), patch('githooks._get_output') as get_output: + get_output.return_value = 'M\texample.py\n' + self.assertEqual(['example.py'], get_commit_files()['M']) + get_output.assert_called_once_with(expected_files_command) + + get_output.reset_mock() + get_output.return_value = '@@ -1 +1 @@\n' + self.assertEqual(['1'], get_changed_lines('example.py')) + get_output.assert_called_once_with(expected_lines_command) + + def test_push_uses_event_before_sha(self): + self._assert_ranges( + 'abc123', + ['git', 'diff', '--ignore-submodules', '--name-status', 'abc123..HEAD', '--'], + ['git', 'diff', '--unified=0', 'abc123..HEAD', '--', 'example.py'] + ) + + def test_initial_push_uses_root_diff(self): + self._assert_ranges( + '0' * 40, + ['git', 'diff-tree', '--root', '--no-commit-id', '--name-status', '-r', 'HEAD', '--'], + ['git', 'diff-tree', '--root', '--no-commit-id', '--unified=0', '-r', 'HEAD', '--', 'example.py'] + ) + + def test_push_without_before_uses_previous_commit(self): + self._assert_ranges( + None, + ['git', 'diff', '--ignore-submodules', '--name-status', 'HEAD~..', '--'], + ['git', 'diff', '--unified=0', 'HEAD~', '--', 'example.py'] + ) + + def yield_changed_lines(changed_lines): '''Yield individual line numbers from list returned by get_changed_lines''' for line_num_range in changed_lines: diff --git a/templates/compliance.yml b/templates/compliance.yml index eedd405..fc618cd 100644 --- a/templates/compliance.yml +++ b/templates/compliance.yml @@ -25,12 +25,15 @@ jobs: - name: Extract commit message shell: bash run: | - echo 'commit_message<> "$GITHUB_ENV" - git log --format=%B -n 1 HEAD >> "$GITHUB_ENV" - echo 'EOF' >> "$GITHUB_ENV" + delimiter="$(python -c 'import uuid; print(uuid.uuid4())')" + { + echo "commit_message<<${delimiter}" + git log --format=%B -n 1 HEAD + echo "${delimiter}" + } >> "$GITHUB_ENV" - name: Run CCDC Commit Hooks & Header Checks - uses: ccdc-opensource/commit-hooks@main + uses: ccdc-opensource/commit-hooks@v8 with: commitMessage: ${{ env.commit_message }} licenseCheck: true From 8a3e0b3a8d7a10f6763bf096bf4259eb3a3b782b Mon Sep 17 00:00:00 2001 From: Manish Maharjan <112875432+mmaharjan-ccdc@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:47:05 +0100 Subject: [PATCH 15/15] SYS-8665 use setup-python@v7 --- .github/workflows/quality_check.yml | 2 +- .github/workflows/status_check.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/quality_check.yml b/.github/workflows/quality_check.yml index 368b331..050b7e6 100644 --- a/.github/workflows/quality_check.yml +++ b/.github/workflows/quality_check.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v7 with: python-version: "3.11" - name: Install dependencies diff --git a/.github/workflows/status_check.yml b/.github/workflows/status_check.yml index 0ff6811..a86b48f 100644 --- a/.github/workflows/status_check.yml +++ b/.github/workflows/status_check.yml @@ -11,7 +11,7 @@ jobs: with: ref: ${{ github.head_ref }} fetch-depth: 0 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v7 with: python-version: "3.11" - name: Get the commit message