From ccc7b2505896df9274c82f17e12736b4f4b26f7b Mon Sep 17 00:00:00 2001 From: Purshottam Date: Wed, 5 Feb 2025 18:56:40 +0530 Subject: [PATCH 1/5] feat(pre-commit): add hook to enforce branch name validation rules --- .github/workflows/pre-commit.yml | 19 +++++- .pre-commit-config.yaml | 9 +++ scripts/check-branch-name.sh | 107 +++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 3 deletions(-) create mode 100755 scripts/check-branch-name.sh diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 0597de7d..9ca3b487 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -9,6 +9,19 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 - - uses: pre-commit/action@v3.0.1 + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v3 + + - name: Install pre-commit + run: | + python -m pip install --upgrade pip + pip install pre-commit + + - name: Install pre-commit hooks + run: pre-commit install + + - name: Run pre-commit hooks + uses: pre-commit/action@v3.0.1 # This will run the pre-commit hooks as configured in your `.pre-commit-config.yml` diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 494fd91e..37f625eb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,3 +10,12 @@ repos: - id: commitlint stages: [commit-msg] additional_dependencies: ['@commitlint/config-conventional'] + + # Local repository for a custom hook + - repo: local + hooks: + - id: check-branch-name + name: 'Check Branch Name' + entry: ./scripts/check-branch-name.sh + language: system + files: ^.* diff --git a/scripts/check-branch-name.sh b/scripts/check-branch-name.sh new file mode 100755 index 00000000..14b4252c --- /dev/null +++ b/scripts/check-branch-name.sh @@ -0,0 +1,107 @@ +#!/bin/sh + +# Define allowed prefixes including release +allowed_prefixes="feat|feature|bugfix|hotfix|chore|refactor|test|spike|prototype|release" + +# Define the JIRA ticket pattern (CSN-XXXX, any number of digits) +jira_ticket_pattern="CSN-[0-9]+" + +# Define the valid release version pattern (release/vX.Y.Z or release/vX.Y.Z-beta) +release_version_pattern="release/v[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9]+)?" + +# Get the current branch name +branch_name=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null) + +# Define the valid hotfix version pattern (hotfix/vX.Y.Z) +hotfix_version_pattern="hotfix/v[0-9]+\.[0-9]+\.[0-9]+" + +# If branch_name is empty or you are in a detached HEAD state where HEAD is returned +if [ -z "$branch_name" ] || [ "$branch_name" = "HEAD" ]; then + printf "❌ ERROR: Unable to determine branch name.\n" + exit 1 +fi + +# Check if it's a valid release branch (release/vX.Y.Z) +if echo "$branch_name" | grep -Eq "^$release_version_pattern$"; then + printf "✅ Branch name is valid (Release branch): '%s'\n" "$branch_name" + exit 0 +fi + +# Validate hotfix branches +if echo "$branch_name" | grep -Eq "^hotfix/"; then + if echo "$branch_name" | grep -Eq "^$hotfix_version_pattern-$jira_ticket_pattern-[a-z0-9]+(-[a-z0-9]+)*$"; then + printf "✅ Branch name is valid (Hotfix branch): '%s'\n" "$branch_name" + exit 0 + else + printf "❌ ERROR: Hotfix branch name '%s' is invalid. Expected format is 'hotfix/vX.Y.Z-CSN-XXXX-description' where 'XXXX' can be any number of digits.\n" "$branch_name" + printf "🔹 Suggested fix: 'hotfix/v1.2.3-CSN-1234-fix-login-issue'\n" + printf "👉 Branch name should follow the format: 'hotfix/vX.Y.Z-CSN-XXXX-description'\n" + exit 1 + fi +fi + +# ❌ **If it starts with `release/` but is NOT valid, show a specific error** +if echo "$branch_name" | grep -Eq "^release/"; then + printf "❌ ERROR: Branch name '%s' is invalid - Release branches must follow the format 'release/vX.Y.Z' or 'release/vX.Y.Z-beta'\n" "$branch_name" >&2 + printf "🔹 Suggested fix: 'release/v1.2.3'\n" + printf "👉 Branch name should follow the format: 'release/vX.Y.Z' or 'release/vX.Y.Z-beta'\n" + exit 1 +fi + +# Extract JIRA ticket (always uppercase CSN-XXXX) +jira_ticket=$(echo "$branch_name" | grep -o "$jira_ticket_pattern") + +# Extract the description (everything after `CSN-XXXX-`) +branch_description=$(echo "$branch_name" | sed -E "s/^($allowed_prefixes)\/$jira_ticket_pattern-//g") + +# Ensure description is lowercase (ignore the `CSN-XXXX` part) +branch_description_lower=$(echo "$branch_description" | tr '[:upper:]' '[:lower:]') + +# Suggest a valid branch name (default to feat/CSN-1234-description if missing details) +if [ -n "$jira_ticket" ] && [ -n "$branch_description" ]; then + suggested_branch_name="feat/${jira_ticket}-$(echo "$branch_description_lower" | tr '_' '-' | tr ' ' '-' | tr -d '[^a-z0-9-]')" +else + suggested_branch_name="feat/CSN-1234-description" +fi + +# Function to display errors properly (single-line for Git popups) +show_error() { + # Print error message on the first line + printf "❌ ERROR: %s\n" "$1" >&2 + + # Print suggested fix on a new line + printf "🔹 Suggested fix: '%s'\n" "$suggested_branch_name" >&2 + + # Print additional information (e.g., branch name format) + printf "👉 Branch name should follow the format: '/CSN-XXXX-description'\n" >&2 + + exit 1 +} + +# Check if the branch name starts with an allowed prefix +if ! echo "$branch_name" | grep -Eq "^($allowed_prefixes)/"; then + show_error "Branch name '$branch_name' is invalid - Must start with one of: $allowed_prefixes/" +fi + +# Check if the branch name contains a valid JIRA ticket (CSN-XXXX) +if ! echo "$branch_name" | grep -Eq "/$jira_ticket_pattern-"; then + show_error "Branch name '$branch_name' is invalid - Must include a JIRA ticket like 'CSN-XXXX' (e.g., CSN-1234, CSN-98765)" +fi + +# Check for uppercase letters in the description (ignore the JIRA ticket itself) +if [ "$branch_description" != "$branch_description_lower" ]; then + show_error "Branch name '$branch_name' contains uppercase letters in the description - Use only lowercase letters, numbers, and hyphens" +fi + +# Ensure kebab-case (no underscores, spaces, or special characters other than '-') +if echo "$branch_description" | grep -Eq "[^a-z0-9-]"; then + show_error "Branch name '$branch_name' contains invalid characters in the description - Only lowercase letters, numbers, and hyphens are allowed" +fi + +# Ensure proper formatting (e.g., feat/CSN-1234-description) +if ! echo "$branch_name" | grep -Eq "^($allowed_prefixes)/$jira_ticket_pattern-[a-z0-9]+(-[a-z0-9]+)*$"; then + show_error "Branch name '$branch_name' is invalid - Format should be '/CSN-XXXX-description' - Example: 'feat/CSN-1234-add-new-feature'" +fi + +printf "✅ Branch name is valid: '%s'\n" "$branch_name" +exit 0 From 968ac8a3a35b5838d165b72b413807acf708969b Mon Sep 17 00:00:00 2001 From: Purshottam Date: Wed, 5 Feb 2025 22:59:35 +0530 Subject: [PATCH 2/5] feat(pre-commit): doc branch added to rule --- scripts/check-branch-name.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-branch-name.sh b/scripts/check-branch-name.sh index 14b4252c..de38b0d1 100755 --- a/scripts/check-branch-name.sh +++ b/scripts/check-branch-name.sh @@ -1,7 +1,7 @@ #!/bin/sh # Define allowed prefixes including release -allowed_prefixes="feat|feature|bugfix|hotfix|chore|refactor|test|spike|prototype|release" +allowed_prefixes="feat|feature|bugfix|hotfix|chore|refactor|test|spike|prototype|release|docs" # Define the JIRA ticket pattern (CSN-XXXX, any number of digits) jira_ticket_pattern="CSN-[0-9]+" From 92405cc86015aaf3268fabea498ac1783e2c60f5 Mon Sep 17 00:00:00 2001 From: Ivan Anishchuk Date: Thu, 6 Feb 2025 11:58:26 +0700 Subject: [PATCH 3/5] feat(pre-commit): separate branch name retrieving and validation Two separate scripts: one detects current value, the other validates it, no dependency on git or files or anything in the validation script. + Configs cleanup. --- .github/workflows/pre-commit.yml | 19 +++---------------- .pre-commit-config.yaml | 23 ++++++++++++++++++----- scripts/check-branch-name.sh | 2 +- scripts/check-current-branch.sh | 6 ++++++ 4 files changed, 28 insertions(+), 22 deletions(-) create mode 100755 scripts/check-current-branch.sh diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 9ca3b487..0597de7d 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -9,19 +9,6 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v3 - - - name: Install pre-commit - run: | - python -m pip install --upgrade pip - pip install pre-commit - - - name: Install pre-commit hooks - run: pre-commit install - - - name: Run pre-commit hooks - uses: pre-commit/action@v3.0.1 # This will run the pre-commit hooks as configured in your `.pre-commit-config.yml` + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 37f625eb..b9dfbaea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,19 +3,32 @@ repos: rev: v5.0.0 hooks: - id: trailing-whitespace + stages: [pre-commit, manual] - id: end-of-file-fixer + stages: [pre-commit, manual] - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook rev: v9.20.0 hooks: - - id: commitlint - stages: [commit-msg] - additional_dependencies: ['@commitlint/config-conventional'] + - id: commitlint + stages: [commit-msg] # Local repository for a custom hook - repo: local hooks: - id: check-branch-name name: 'Check Branch Name' - entry: ./scripts/check-branch-name.sh + entry: ./scripts/check-current-branch.sh + stages: [pre-commit, pre-push, manual] language: system - files: ^.* + files: /dev/null + always_run: true + verbose: true + - repo: local + hooks: + - id: post-checkout-check + name: 'Check Branch Name' + entry: ./scripts/check-current-branch.sh + stages: [post-checkout] + language: system + always_run: true + verbose: true diff --git a/scripts/check-branch-name.sh b/scripts/check-branch-name.sh index de38b0d1..64f7719b 100755 --- a/scripts/check-branch-name.sh +++ b/scripts/check-branch-name.sh @@ -10,7 +10,7 @@ jira_ticket_pattern="CSN-[0-9]+" release_version_pattern="release/v[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9]+)?" # Get the current branch name -branch_name=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null) +branch_name="$1" # Define the valid hotfix version pattern (hotfix/vX.Y.Z) hotfix_version_pattern="hotfix/v[0-9]+\.[0-9]+\.[0-9]+" diff --git a/scripts/check-current-branch.sh b/scripts/check-current-branch.sh new file mode 100755 index 00000000..785261d9 --- /dev/null +++ b/scripts/check-current-branch.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +# Get the current branch name +branch_name="${GITHUB_HEAD_REF:-$(git branch --show-current)}" + +scripts/check-branch-name.sh "$branch_name" From 7a1b165d71e2972501ba8ef0d7b5eb51e0a0da83 Mon Sep 17 00:00:00 2001 From: Ivan Anishchuk Date: Thu, 6 Feb 2025 12:45:07 +0700 Subject: [PATCH 4/5] feat(pre-commit): add a script to install additional git repo hooks I find post-checkout and pre-push the most effective places for branch name check - after actual branch creation and before things get pushed. --- QA.md | 2 +- scripts/install-repo-hooks.sh | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100755 scripts/install-repo-hooks.sh diff --git a/QA.md b/QA.md index f07fe934..db2cde0d 100644 --- a/QA.md +++ b/QA.md @@ -2,7 +2,7 @@ We are starting to introduce pre-commit and adding checks to it and enabling GitHub Actions. -Locally, start by installing pre-commit package and running `pre-commit install --install-hooks`. It will ensure checksare being run before each commit is made. +Locally, start by installing pre-commit package and running `scripts/install-repo-hooks.sh` - it will ensure checks are being run before each commit is made and in other situations. Autolinting commits (made after running `pre-commit run -a` and fixing all files with new checks) are to be recorded in `.git-blame-ignore-revs` and that file can be used with git blame and git config snippet like this (or command-line `--ignore-revs-file`) to skip such commits when examining history. diff --git a/scripts/install-repo-hooks.sh b/scripts/install-repo-hooks.sh new file mode 100755 index 00000000..601e30a0 --- /dev/null +++ b/scripts/install-repo-hooks.sh @@ -0,0 +1,7 @@ +#!/bin/bash +python3 -m pip install pre-commit +pre-commit install --install-hooks +pre-commit install -t commit-msg +pre-commit install -t post-checkout +pre-commit install -t pre-commit +pre-commit install -t pre-push From d540678f8af100e76665158b84b7401d79a46668 Mon Sep 17 00:00:00 2001 From: Purshottam Date: Mon, 10 Feb 2025 09:21:13 +0530 Subject: [PATCH 5/5] feat(pre-commit): enforce fix over bugfix and feat over feature --- scripts/check-branch-name.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-branch-name.sh b/scripts/check-branch-name.sh index 64f7719b..0804e093 100755 --- a/scripts/check-branch-name.sh +++ b/scripts/check-branch-name.sh @@ -1,7 +1,7 @@ #!/bin/sh # Define allowed prefixes including release -allowed_prefixes="feat|feature|bugfix|hotfix|chore|refactor|test|spike|prototype|release|docs" +allowed_prefixes="feat|fix|hotfix|chore|refactor|test|spike|prototype|release|docs" # Define the JIRA ticket pattern (CSN-XXXX, any number of digits) jira_ticket_pattern="CSN-[0-9]+"