├── .husky ├── .gitignore └── pre-commit ├── .npmrc ├── .gitattributes ├── .gitignore ├── .editorconfig ├── cli.js ├── .eslintrc.js ├── .github ├── branch-name-lint.json └── workflows │ ├── npm-publish.yml │ └── main.yml ├── tests ├── sample-configuration.json ├── sample-configuration.js ├── e2e-test.js └── test.js ├── license ├── CHANGELOG.md ├── package.json ├── bin └── branch-name-lint ├── index.js └── readme.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | .nyc_output 4 | coverage 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const childProcess = require('child_process'); 4 | 5 | process.emitWarning('branch-name-lint will stop using ./cli.js path in the future. Please use npx branch-name-lint'); 6 | 7 | childProcess.fork('./bin/branch-name-lint'); 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'airbnb-base', 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 12, 12 | }, 13 | rules: { 14 | }, 15 | settings: { 16 | 'import/resolver': { 17 | node: { 18 | extensions: ['.js'], 19 | }, 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.github/branch-name-lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "branchNameLinter": { 3 | "prefixes": ["feature", "hotfix", "release", "docs", "chore", "fix", "ci", "test", "refactor", "perf"], 4 | "suggestions": { 5 | "features": "feature", 6 | "feat": "feature", 7 | "fix": "hotfix", 8 | "releases": "release" 9 | }, 10 | "banned": ["wip"], 11 | "skip": ["main", "master", "develop", "staging"], 12 | "disallowed": [], 13 | "separator": "/", 14 | "branchNameEnvVariable": "GITHUB_BRANCH_NAME", 15 | "msgBranchBanned": "Branches with the name \"%s\" are not allowed.", 16 | "msgBranchDisallowed": "Pushing to \"%s\" is not allowed, use git-flow.", 17 | "msgPrefixNotAllowed": "Branch prefix \"%s\" is not allowed.", 18 | "msgPrefixSuggestion": "Instead of \"%s\" try \"%s\".", 19 | "msgSeparatorRequired": "Branch \"%s\" must contain a separator \"%s\"." 20 | } 21 | } -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 18 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 18 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | 35 | -------------------------------------------------------------------------------- /tests/sample-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "branchNameLinter": { 3 | "prefixes": [ 4 | "feature", 5 | "hotfix", 6 | "release", 7 | "bugfix", 8 | "issue" 9 | ], 10 | "suggestions": { 11 | "features": "feature", 12 | "feat": "feature", 13 | "releases": "release" 14 | }, 15 | "banned": [ 16 | "wip" 17 | ], 18 | "skip": [ 19 | "skip-ci" 20 | ], 21 | "disallowed": [ 22 | "master", 23 | "develop", 24 | "staging" 25 | ], 26 | "separator": "/", 27 | "msgBranchBanned": "Branches with the name \"%s\" are not allowed.", 28 | "msgBranchDisallowed": "Pushing to \"%s\" is not allowed, use git-flow.", 29 | "msgPrefixNotAllowed": "Branch prefix \"%s\" is not allowed.", 30 | "msgPrefixSuggestion": "Instead of \"%s\" try \"%s\".", 31 | "msgseparatorRequiredL": "Branch \"%s\" must contain a separator \"%s\"." 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Ran Bar-Zik (internet-israel.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /tests/sample-configuration.js: -------------------------------------------------------------------------------- 1 | // Sample JavaScript configuration file for branch-name-lint 2 | // This demonstrates the ability to use JavaScript variables and imports 3 | 4 | // Define constants that can be reused 5 | const COMMON_PREFIXES = ['feature', 'bugfix', 'hotfix', 'release']; 6 | const CI_PREFIXES = ['ci', 'build']; 7 | 8 | // Combine arrays for configuration 9 | const ALL_PREFIXES = [...COMMON_PREFIXES, ...CI_PREFIXES]; 10 | 11 | // Export the configuration object 12 | module.exports = { 13 | // Set prefixes to false to disable prefix check 14 | // prefixes: false, 15 | prefixes: ALL_PREFIXES, 16 | suggestions: { 17 | feat: 'feature' 18 | }, 19 | banned: ['wip', 'tmp'], 20 | skip: ['develop', 'master', 'main'], 21 | // Set separator to false to disable separator check 22 | // separator: false, 23 | separator: '/', 24 | disallowed: ['master', 'develop', 'main'], 25 | regex: '^[a-z0-9/-]+$', 26 | regexOptions: 'i', 27 | msgBranchBanned: 'Branch name "%s" is banned.', 28 | msgBranchDisallowed: 'Branch name "%s" is disallowed.', 29 | msgPrefixNotAllowed: 'Branch prefix "%s" is not allowed.', 30 | msgSeparatorRequired: 'Branch "%s" must contain a separator "%s".', 31 | msgDoesNotMatchRegex: 'Branch "%s" does not match regex pattern: %s', 32 | }; -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Branch Lint Name CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | - macos-latest 19 | - windows-latest 20 | node_version: 21 | - 18 22 | - 20 23 | - 22 24 | architecture: 25 | - x64 26 | name: Branch Lint Name ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }} 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Setup node 30 | uses: actions/setup-node@v2 31 | with: 32 | node-version: ${{ matrix.node_version }} 33 | architecture: ${{ matrix.architecture }} 34 | - run: npm install 35 | - name: Extract branch name 36 | shell: bash 37 | run: | 38 | if [ "${{ github.event_name }}" == "pull_request" ]; then 39 | # For pull requests, use the head branch name 40 | echo "BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV 41 | else 42 | # For pushes, extract from GITHUB_REF 43 | echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV 44 | fi 45 | - name: Check branch name 46 | run: npx branch-name-lint .github/branch-name-lint.json 47 | env: 48 | GITHUB_BRANCH_NAME: ${{ env.BRANCH_NAME }} 49 | - run: npm test 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [v3.0.1] - 2025-04-25 10 | ### Changed 11 | - Updated GitHub workflow to use Node.js 18 12 | 13 | ## [v3.0.0] - 2025-04-25 14 | ### Fixed 15 | - Fix incorrect error message upon regex validation failure. 16 | - Added option for custom branch name 17 | - Added e2e and more testing 18 | - Removed old shrinkwrap 19 | - Better output validation 20 | - Cleaning npm audit results 21 | 22 | ## [2.1.0] - 2021-06-11 23 | ### Added 24 | - optional `regexOptions`. 25 | ### Changed 26 | - Removed `xo` and moved to eslint. 27 | ### Fixed 28 | - Email in license. 29 | - dependencies and dev dependencies versions update 30 | 31 | ## [2.0.0] - 2021-02-03 32 | ### Changed 33 | - Option changed. From `seperator` to `separator`. 34 | - Option changed. From `msgseperatorRequiredL` to `msgseparatorRequiredL`. 35 | - Now case sensitive on branch name and branch prefix. 36 | - main branch instead of master. 37 | 38 | ## [1.4.1] - 2021-01-13 39 | ### Fixed 40 | - Security Dependency check and fix. 41 | - Moved to Github Action. 42 | 43 | ## [1.4.0] - 2020-07-05 44 | ### Added 45 | - Regex support (no breaking change). 46 | 47 | ## [1.2.0] - 2020-05-02 48 | ### Added 49 | - npx support. 50 | 51 | ## [1.1.5] - 2020-05-02 52 | ### Fixed 53 | - Tests on Node.js LTS. 54 | ### Changed 55 | - indexof to include. 56 | ### Added 57 | - Badges and documentation. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "branch-name-lint", 3 | "version": "3.0.1", 4 | "description": "Lint your branch names", 5 | "license": "MIT", 6 | "repository": "barzik/branch-name-lint", 7 | "author": { 8 | "name": "Ran Bar-Zik", 9 | "email": "ran.bar.zik@teamaol.com", 10 | "url": "https://internet-israel.com" 11 | }, 12 | "bin": { 13 | "branch-name-lint": "bin/branch-name-lint" 14 | }, 15 | "engines": { 16 | "node": ">=18" 17 | }, 18 | "husky": { 19 | "hooks": { 20 | "pre-push": "npm test && node ./bin/branch-name-lint tests/sample-configuration.json" 21 | } 22 | }, 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "scripts": { 27 | "test": "node tests/test.js && npm run e2e", 28 | "e2e": "node ./tests/e2e-test.js", 29 | "prepack": "npm prune --production && rm package-lock.json && npm shrinkwrap", 30 | "postpack": "git checkout package-lock.json && git clean -f npm-shrinkwrap.json", 31 | "release": "npm publish", 32 | "prerelease": "npm version patch && git push --follow-tags --no-verify", 33 | "lint": "npx eslint *.js --ignore-path .gitignore" 34 | }, 35 | "files": [ 36 | "index.js", 37 | "cli.js" 38 | ], 39 | "keywords": [ 40 | "cli-app", 41 | "cli", 42 | "branch-name-lint", 43 | "lint", 44 | "validate", 45 | "branch" 46 | ], 47 | "dependencies": { 48 | "find-up": "^5.0.0", 49 | "meow": "^9.0.0" 50 | }, 51 | "devDependencies": { 52 | "eslint": "^7.28.0", 53 | "eslint-config-airbnb-base": "^14.2.1", 54 | "eslint-plugin-import": "^2.23.4", 55 | "husky": "^6.0.0", 56 | "nyc": "^15.1.0", 57 | "rimraf": "^6.0.1", 58 | "sinon": "^11.1.1" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /bin/branch-name-lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const findUp = require('find-up'); 6 | const meow = require('meow'); 7 | const BranchNameLint = require('../index'); 8 | 9 | const cli = meow(` 10 | Usage 11 | $ npx branch-name-lint [configuration-file.json|configuration-file.js] 12 | 13 | Options 14 | --help - to get this screen 15 | --branch - specify a custom branch name to check instead of the current git branch 16 | 17 | Examples 18 | $ branch-name-lint 19 | Use default configutation or the configuration specified in package.json to validate & lint the branch name. 20 | $ branch-name-lint [configuration-file.json|configuration-file.js] 21 | Use configutation file to validate & lint the branch name. 22 | $ branch-name-lint --branch feature/my-new-feature 23 | Validate a specific branch name. 24 | `, { 25 | flags: { 26 | branch: { 27 | type: 'string' 28 | } 29 | } 30 | }); 31 | const configFileName = cli.input[0]; 32 | 33 | class BranchNameLintCli { 34 | constructor() { 35 | this.options = this.loadConfiguration(configFileName); 36 | 37 | // Apply command line branch option if provided 38 | if (cli.flags.branch) { 39 | this.options.branch = cli.flags.branch; 40 | } 41 | 42 | const branchNameLint = new BranchNameLint(this.options); 43 | const answer = branchNameLint.doValidation(); 44 | if (answer === 1) { 45 | process.exit(1); 46 | } 47 | } 48 | 49 | loadConfiguration(filename = 'package.json') { 50 | const pkgFile = findUp.sync(filename); 51 | if (!pkgFile) { 52 | console.error(`Could not find configuration file: ${filename}`); 53 | process.exit(1); 54 | } 55 | 56 | const fileExtension = path.extname(pkgFile).toLowerCase(); 57 | 58 | // Load JS files using require 59 | if (fileExtension === '.js') { 60 | try { 61 | // Use absolute path for require 62 | const config = require(pkgFile); 63 | return config.branchNameLinter || config; 64 | } catch (error) { 65 | console.error(`Error loading JavaScript configuration: ${error.message}`); 66 | process.exit(1); 67 | } 68 | } 69 | // Load JSON files as before 70 | else { 71 | try { 72 | const pkg = JSON.parse(fs.readFileSync(pkgFile)); 73 | return (pkg.branchNameLinter) || {}; 74 | } catch (error) { 75 | console.error(`Error parsing JSON configuration: ${error.message}`); 76 | process.exit(1); 77 | } 78 | } 79 | } 80 | } 81 | 82 | new BranchNameLintCli(); // eslint-disable-line no-new 83 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const childProcess = require('child_process'); 2 | const util = require('util'); 3 | 4 | class BranchNameLint { 5 | constructor(options) { 6 | const defaultOptions = { 7 | prefixes: ['feature', 'hotfix', 'release'], 8 | suggestions: { 9 | features: 'feature', feat: 'feature', fix: 'hotfix', releases: 'release', 10 | }, 11 | banned: ['wip'], 12 | skip: [], 13 | disallowed: ['master', 'develop', 'staging'], 14 | separator: '/', 15 | msgBranchBanned: 'Branches with the name "%s" are not allowed.', 16 | msgBranchDisallowed: 'Pushing to "%s" is not allowed, use git-flow.', 17 | msgPrefixNotAllowed: 'Branch prefix "%s" is not allowed.', 18 | msgPrefixSuggestion: 'Instead of "%s" try "%s".', 19 | msgseparatorRequired: 'Branch "%s" must contain a separator "%s".', 20 | msgDoesNotMatchRegex: 'Branch "%s" does not match the allowed pattern: "%s"', 21 | branch: false, 22 | branchNameEnvVariable: false, 23 | }; 24 | 25 | this.options = Object.assign(defaultOptions, options); 26 | this.branch = this.getCurrentBranch(); 27 | this.ERROR_CODE = 1; 28 | this.SUCCESS_CODE = 0; 29 | } 30 | 31 | validateWithRegex() { 32 | if (this.options.regex) { 33 | const REGEX = new RegExp(this.options.regex, this.options.regexOptions); 34 | return REGEX.test(this.branch); 35 | } 36 | 37 | return true; 38 | } 39 | 40 | doValidation() { 41 | if (this.options.skip.length > 0 && this.options.skip.includes(this.branch)) { 42 | return this.SUCCESS_CODE; 43 | } 44 | 45 | if (this.options.banned.includes(this.branch)) { 46 | return this.error(this.options.msgBranchBanned, this.branch); 47 | } 48 | 49 | if (this.options.disallowed.includes(this.branch)) { 50 | return this.error(this.options.msgBranchDisallowed, this.branch); 51 | } 52 | 53 | // Skip separator check if separator is explicitly set to false 54 | if (this.options.separator !== false && this.branch.includes(this.options.separator) === false) { 55 | return this.error(this.options.msgseparatorRequired, this.branch, this.options.separator); 56 | } 57 | 58 | if (!this.validateWithRegex()) { 59 | return this.error(this.options.msgDoesNotMatchRegex, this.branch, this.options.regex); 60 | } 61 | 62 | // If separator is disabled, skip the prefix check 63 | if (this.options.separator === false) { 64 | return this.SUCCESS_CODE; 65 | } 66 | 67 | // Process prefix only if separator is enabled 68 | const parts = this.branch.split(this.options.separator); 69 | const prefix = parts[0]; 70 | let name = null; 71 | 72 | if (parts[1]) { 73 | const [, second] = parts; 74 | name = second; 75 | } 76 | 77 | // Skip prefix check if prefixes is explicitly set to false 78 | if (this.options.prefixes !== false && this.options.prefixes.includes(prefix) === false) { 79 | if (this.options.suggestions[prefix]) { 80 | const separator = this.options.separator || '/'; 81 | this.error( 82 | this.options.msgPrefixSuggestion, 83 | [prefix, name].join(separator), 84 | [this.options.suggestions[prefix], name].join(separator), 85 | ); 86 | } else { 87 | this.error(this.options.msgPrefixNotAllowed, prefix); 88 | } 89 | 90 | return this.ERROR_CODE; 91 | } 92 | 93 | return this.SUCCESS_CODE; 94 | } 95 | 96 | getCurrentBranch() { 97 | // First check if a custom branch name was provided via options 98 | if (this.options.branch) { 99 | return this.options.branch; 100 | } 101 | 102 | // Then check if an environment variable name was provided and has a value 103 | if (this.options.branchNameEnvVariable && process.env[this.options.branchNameEnvVariable]) { 104 | return process.env[this.options.branchNameEnvVariable]; 105 | } 106 | 107 | // Fall back to git command if no custom branch name is provided 108 | const branch = childProcess.execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD']).toString(); 109 | this.branch = branch; 110 | return this.branch.trim(); 111 | } 112 | 113 | error() { 114 | /* eslint-disable no-console */ 115 | console.error('Branch name lint fail!', Reflect.apply(util.format, null, arguments)); // eslint-disable-line prefer-rest-params 116 | /* eslint-enable no-console */ 117 | return this.ERROR_CODE; 118 | } 119 | } 120 | 121 | module.exports = BranchNameLint; 122 | -------------------------------------------------------------------------------- /tests/e2e-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * End-to-end test for branch-name-lint 5 | * This script tests the module by: 6 | * 1. Creating a test directory 7 | * 2. Initializing a git repo 8 | * 3. Linking the current module 9 | * 4. Testing with a bad branch name (should fail) 10 | * 5. Testing with a good branch name (should pass) 11 | * 6. Testing with JavaScript configuration file 12 | * 7. Testing with disabled separator and prefix checks 13 | * 8. Testing with --branch CLI option 14 | * 9. Testing with branchNameEnvVariable configuration 15 | * 10. Cleaning up the test directory 16 | */ 17 | 18 | const fs = require('fs'); 19 | const path = require('path'); 20 | const { execSync } = require('child_process'); 21 | const rimraf = require('rimraf'); 22 | 23 | // Configuration 24 | const TEST_DIR = path.join(__dirname, 'e2e-test-dir'); 25 | const GOOD_BRANCH_NAME = 'feature/valid-branch'; 26 | const BAD_BRANCH_NAME = 'invalid-branch'; 27 | const CI_BRANCH_NAME = 'ci/build-test'; // For testing JS config with extra prefixes 28 | const NO_SEPARATOR_BRANCH = 'anyprefix-no-separator'; // For testing disabled separator check 29 | const JSON_CONFIG_PATH = path.join(__dirname, 'sample-configuration.json'); 30 | const JS_CONFIG_PATH = path.join(__dirname, 'sample-configuration.js'); 31 | const DISABLED_CONFIG_PATH = path.join(TEST_DIR, 'disabled-config.json'); 32 | const ENV_VAR_CONFIG_PATH = path.join(TEST_DIR, 'env-var-config.json'); // For testing branchNameEnvVariable 33 | const PROJECT_ROOT = path.join(__dirname, '..'); 34 | 35 | // Utility function to execute commands with better error handling 36 | function runCommand(cmd, options = {}) { 37 | try { 38 | const result = execSync(cmd, { encoding: 'utf8', stdio: 'pipe', ...options }); 39 | console.log(`✅ Successfully ran: ${cmd}`); 40 | return { success: true, output: result }; 41 | } catch (error) { 42 | if (options.expectError) { 43 | console.log(`✅ Expected error from: ${cmd}`); 44 | return { success: false, error }; 45 | } 46 | console.error(`❌ Error running: ${cmd}`); 47 | console.error(error.message); 48 | process.exit(1); 49 | } 50 | } 51 | 52 | // Create test directory 53 | console.log('\n🚀 Starting branch-name-lint end-to-end test\n'); 54 | console.log('1️⃣ Creating test directory...'); 55 | if (fs.existsSync(TEST_DIR)) { 56 | rimraf.sync(TEST_DIR); 57 | } 58 | fs.mkdirSync(TEST_DIR); 59 | 60 | // Move to test directory 61 | process.chdir(TEST_DIR); 62 | console.log(` Changed to directory: ${TEST_DIR}`); 63 | 64 | // Initialize git repo 65 | console.log('\n2️⃣ Initializing git repository...'); 66 | runCommand('git init'); 67 | runCommand('git config user.email "e2e-test@example.com"'); 68 | runCommand('git config user.name "E2E Test"'); 69 | 70 | // Create a dummy file and commit it 71 | fs.writeFileSync(path.join(TEST_DIR, 'dummy.txt'), 'Initial commit file'); 72 | runCommand('git add dummy.txt'); 73 | runCommand('git commit -m "Initial commit"'); 74 | 75 | // Link the current module 76 | console.log('\n3️⃣ Linking branch-name-lint module...'); 77 | runCommand(`npm link "${PROJECT_ROOT}"`); 78 | 79 | // Test with BAD branch name 80 | console.log('\n4️⃣ Testing with BAD branch name...'); 81 | runCommand(`git checkout -b ${BAD_BRANCH_NAME}`); 82 | console.log(` Created branch: ${BAD_BRANCH_NAME}`); 83 | 84 | // Run branch-name-lint - should fail 85 | console.log('\n Running branch-name-lint with JSON config - expecting failure:'); 86 | const badResult = runCommand(`npx branch-name-lint ${JSON_CONFIG_PATH}`, { expectError: true }); 87 | if (!badResult.success) { 88 | console.log(' ✅ Lint correctly failed for invalid branch name with JSON config'); 89 | } else { 90 | console.error(' ❌ Lint unexpectedly passed for invalid branch name'); 91 | process.exit(1); 92 | } 93 | 94 | // Test with GOOD branch name 95 | console.log('\n5️⃣ Testing with GOOD branch name...'); 96 | runCommand(`git checkout -b ${GOOD_BRANCH_NAME}`); 97 | console.log(` Created branch: ${GOOD_BRANCH_NAME}`); 98 | 99 | // Run branch-name-lint - should pass 100 | console.log('\n Running branch-name-lint with JSON config - expecting success:'); 101 | runCommand(`npx branch-name-lint ${JSON_CONFIG_PATH}`); 102 | console.log(' ✅ Lint correctly passed for valid branch name with JSON config'); 103 | 104 | // Test with JavaScript config file 105 | console.log('\n6️⃣ Testing with JavaScript configuration file...'); 106 | 107 | // Test with a CI branch name which is only valid in the JS config that has extended prefixes 108 | runCommand(`git checkout -b ${CI_BRANCH_NAME}`); 109 | console.log(` Created branch: ${CI_BRANCH_NAME}`); 110 | 111 | // This should fail with JSON config (doesn't have 'ci' prefix) 112 | console.log('\n Running branch-name-lint with JSON config for CI branch - expecting failure:'); 113 | const ciWithJsonResult = runCommand(`npx branch-name-lint ${JSON_CONFIG_PATH}`, { expectError: true }); 114 | if (!ciWithJsonResult.success) { 115 | console.log(' ✅ Lint correctly failed for CI branch with JSON config'); 116 | } else { 117 | console.error(' ❌ Lint unexpectedly passed for CI branch with JSON config'); 118 | process.exit(1); 119 | } 120 | 121 | // But should pass with JavaScript config (has 'ci' prefix) 122 | console.log('\n Running branch-name-lint with JavaScript config for CI branch - expecting success:'); 123 | runCommand(`npx branch-name-lint ${JS_CONFIG_PATH}`); 124 | console.log(' ✅ Lint correctly passed for CI branch with JavaScript config'); 125 | 126 | // Test validation still works correctly with JS config 127 | console.log('\n Testing that validation still works with JavaScript config'); 128 | runCommand(`git checkout -b ${BAD_BRANCH_NAME}-js-test`); 129 | const badWithJsResult = runCommand(`npx branch-name-lint ${JS_CONFIG_PATH}`, { expectError: true }); 130 | if (!badWithJsResult.success) { 131 | console.log(' ✅ Lint correctly failed for invalid branch with JavaScript config'); 132 | } else { 133 | console.error(' ❌ Lint unexpectedly passed for invalid branch with JavaScript config'); 134 | process.exit(1); 135 | } 136 | 137 | // Test with disabled separator and prefix checks 138 | console.log('\n7️⃣ Testing with disabled separator and prefix checks...'); 139 | 140 | // Create a configuration file with disabled separator and prefix checks 141 | const disabledConfig = { 142 | branchNameLinter: { 143 | prefixes: false, 144 | separator: false, 145 | regex: ".*" // Use a permissive regex pattern that allows any string 146 | } 147 | }; 148 | 149 | fs.writeFileSync(DISABLED_CONFIG_PATH, JSON.stringify(disabledConfig, null, 2)); 150 | console.log(` Created disabled configuration at: ${DISABLED_CONFIG_PATH}`); 151 | 152 | // Create a branch without separator - this would normally fail but should pass with disabled checks 153 | runCommand(`git checkout -b ${NO_SEPARATOR_BRANCH}`); 154 | console.log(` Created branch without separator: ${NO_SEPARATOR_BRANCH}`); 155 | 156 | // Run branch-name-lint with disabled config - should pass despite no separator and invalid prefix 157 | console.log('\n Running branch-name-lint with disabled checks - expecting success:'); 158 | runCommand(`npx branch-name-lint ${DISABLED_CONFIG_PATH}`); 159 | console.log(' ✅ Lint correctly passed for branch with disabled checks'); 160 | 161 | // Test with --branch CLI option 162 | console.log('\n8️⃣ Testing with --branch CLI option...'); 163 | 164 | // Test with an invalid branch name via CLI (should be on a valid git branch now) 165 | console.log('\n Testing --branch with an invalid branch name:'); 166 | const badCliResult = runCommand(`npx branch-name-lint ${JSON_CONFIG_PATH} --branch invalid-cli-branch`, { expectError: true }); 167 | if (!badCliResult.success) { 168 | console.log(' ✅ Lint correctly failed for invalid branch name provided via --branch'); 169 | } else { 170 | console.error(' ❌ Lint unexpectedly passed for invalid branch name provided via --branch'); 171 | process.exit(1); 172 | } 173 | 174 | // Test with a valid branch name via CLI 175 | console.log('\n Testing --branch with a valid branch name:'); 176 | runCommand(`npx branch-name-lint ${JSON_CONFIG_PATH} --branch feature/valid-cli-branch`); 177 | console.log(' ✅ Lint correctly passed for valid branch name provided via --branch'); 178 | 179 | // Test with branchNameEnvVariable configuration 180 | console.log('\n9️⃣ Testing with branchNameEnvVariable configuration...'); 181 | 182 | // Create a configuration file that specifies an environment variable 183 | const envVarConfig = { 184 | branchNameLinter: { 185 | branchNameEnvVariable: "TEST_BRANCH_NAME", 186 | prefixes: ["feature", "hotfix", "release"] 187 | } 188 | }; 189 | 190 | fs.writeFileSync(ENV_VAR_CONFIG_PATH, JSON.stringify(envVarConfig, null, 2)); 191 | console.log(` Created environment variable configuration at: ${ENV_VAR_CONFIG_PATH}`); 192 | 193 | // Test with an invalid branch name via environment variable 194 | console.log('\n Testing branchNameEnvVariable with an invalid branch name:'); 195 | const badEnvVarResult = runCommand(`npx branch-name-lint ${ENV_VAR_CONFIG_PATH}`, { 196 | expectError: true, 197 | env: { ...process.env, TEST_BRANCH_NAME: "invalid-env-branch" } 198 | }); 199 | if (!badEnvVarResult.success) { 200 | console.log(' ✅ Lint correctly failed for invalid branch name provided via environment variable'); 201 | } else { 202 | console.error(' ❌ Lint unexpectedly passed for invalid branch name provided via environment variable'); 203 | process.exit(1); 204 | } 205 | 206 | // Test with a valid branch name via environment variable 207 | console.log('\n Testing branchNameEnvVariable with a valid branch name:'); 208 | runCommand(`npx branch-name-lint ${ENV_VAR_CONFIG_PATH}`, { 209 | env: { ...process.env, TEST_BRANCH_NAME: "feature/valid-env-branch" } 210 | }); 211 | console.log(' ✅ Lint correctly passed for valid branch name provided via environment variable'); 212 | 213 | // Clean up 214 | console.log('\n🔟 Cleaning up...'); 215 | process.chdir(PROJECT_ROOT); 216 | rimraf.sync(TEST_DIR); 217 | console.log(` Removed test directory: ${TEST_DIR}`); 218 | 219 | console.log('\n✨ End-to-end test completed successfully! ✨\n'); -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # branch-name-lint ![Build Status](https://github.com/barzik/branch-name-lint/workflows/Branch%20Lint%20Name%20CI/badge.svg) [![Known Vulnerabilities](https://snyk.io/test/github/barzik/branch-name-lint/badge.svg)](https://snyk.io/test/github/barzik//branch-name-lint) ![npm](https://img.shields.io/npm/dt/branch-name-lint) 2 | 3 | Validating and linting the git branch name. Create a config file or use the default configuration file. Use it in husky config file to make sure that your branch will not be rejected by some pesky Jenkins branch name conventions. You may use it as part of a CI process or just as an handy `npx` command. 4 | 5 | ## Install 6 | 7 | ``` 8 | $ npm install branch-name-lint 9 | ``` 10 | 11 | ## CLI usage 12 | 13 | ``` 14 | $ npx branch-name-lint 15 | ``` 16 | 17 | ``` 18 | $ npx branch-name-lint --help 19 | 20 | Usage 21 | npx branch-name-lint [configfileLocation JSON|JS] 22 | 23 | Options 24 | --help - to get this screen 25 | --branch - specify a custom branch name to check instead of the current git branch 26 | 27 | Examples 28 | $ branch-name-lint 29 | $ branch-name-lint config-file.json 30 | $ branch-name-lint config-file.js 31 | $ branch-name-lint --branch feature/my-new-feature 32 | ``` 33 | 34 | ### CLI options.json 35 | 36 | Any Valid JSON file with `branchNameLinter` attribute. 37 | 38 | ``` 39 | { 40 | "branchNameLinter": { 41 | "prefixes": [ 42 | "feature", 43 | "hotfix", 44 | "release" 45 | ], 46 | "suggestions": { 47 | "features": "feature", 48 | "feat": "feature", 49 | "fix": "hotfix", 50 | "releases": "release" 51 | }, 52 | "banned": [ 53 | "wip" 54 | ], 55 | "skip": [ 56 | "skip-ci" 57 | ], 58 | "disallowed": [ 59 | "master", 60 | "develop", 61 | "staging" 62 | ], 63 | "separator": "/", 64 | "branchNameEnvVariable": false, 65 | "branch": false, 66 | "msgBranchBanned": "Branches with the name \"%s\" are not allowed.", 67 | "msgBranchDisallowed": "Pushing to \"%s\" is not allowed, use git-flow.", 68 | "msgPrefixNotAllowed": "Branch prefix \"%s\" is not allowed.", 69 | "msgPrefixSuggestion": "Instead of \"%s\" try \"%s\".", 70 | "msgSeparatorRequired": "Branch \"%s\" must contain a separator \"%s\"." 71 | } 72 | } 73 | ``` 74 | 75 | ### Specifying a Custom Branch Name 76 | 77 | You can specify a custom branch name to validate instead of using the current git branch in two ways: 78 | 79 | 1. Using the CLI flag: 80 | ``` 81 | $ npx branch-name-lint --branch feature/my-custom-branch 82 | ``` 83 | 84 | 2. Using configuration: 85 | ```json 86 | { 87 | "branchNameLinter": { 88 | "branch": "feature/my-custom-branch" 89 | } 90 | } 91 | ``` 92 | 93 | 3. Using an environment variable: 94 | ```json 95 | { 96 | "branchNameLinter": { 97 | "branchNameEnvVariable": "CI_BRANCH_NAME" 98 | } 99 | } 100 | ``` 101 | 102 | Then set the environment variable: 103 | ``` 104 | CI_BRANCH_NAME=feature/my-custom-branch npx branch-name-lint 105 | ``` 106 | 107 | This is useful for CI/CD environments where you might want to validate branch names from environment variables. 108 | 109 | ### Disabling Checks 110 | 111 | You can disable prefix or separator checks by setting their respective configuration values to `false`: 112 | 113 | ``` 114 | { 115 | "branchNameLinter": { 116 | "prefixes": false, // Disables the prefix validation check 117 | "separator": false, // Disables the separator validation check 118 | "regex": "^(revert|master|develop|issue|release|hotfix/|feature/|support/|shift-)" 119 | } 120 | } 121 | ``` 122 | 123 | When `prefixes` is set to `false`, any branch prefix will be allowed. When `separator` is set to `false`, branches without separators will be allowed. 124 | 125 | ### CLI options.js 126 | 127 | You can also use a JavaScript file for configuration, which allows for more dynamic configuration with variables and imports: 128 | 129 | ```js 130 | // config-file.js 131 | // Define constants that can be reused 132 | const COMMON_PREFIXES = ['feature', 'bugfix', 'hotfix', 'release']; 133 | const CI_PREFIXES = ['ci', 'build']; 134 | 135 | // Combine arrays for configuration 136 | const ALL_PREFIXES = [...COMMON_PREFIXES, ...CI_PREFIXES]; 137 | 138 | // Export the configuration object 139 | module.exports = { 140 | prefixes: ALL_PREFIXES, // Set to false to disable prefix check 141 | suggestions: { 142 | feat: 'feature' 143 | }, 144 | banned: ['wip', 'tmp'], 145 | skip: ['develop', 'master', 'main'], 146 | separator: '/', // Set to false to disable separator check 147 | disallowed: ['master', 'develop', 'main'], 148 | // other options... 149 | }; 150 | ``` 151 | 152 | ## Usage with regex 153 | 154 | In order to check the branch name with a regex you can add a a regex as a string under the branchNameLinter in your config JSON. You can also pass any options for the regex (e.g. case insensitive: 'i') 155 | 156 | ``` 157 | { 158 | "branchNameLinter": { 159 | "regex": "^([A-Z]+-[0-9]+.{5,70})", 160 | "regexOptions": "i", 161 | ... 162 | "msgDoesNotMatchRegex": 'Branch "%s" does not match the allowed pattern: "%s"' 163 | } 164 | } 165 | ``` 166 | 167 | ## Husky usage 168 | 169 | After installation, just add in any husky hook as node modules call. 170 | 171 | ``` 172 | "husky": { 173 | "hooks": { 174 | "pre-push": "npx branch-name-lint [sample-configuration.json]" 175 | } 176 | }, 177 | ``` 178 | 179 | Or with a JavaScript configuration file: 180 | 181 | ``` 182 | "husky": { 183 | "hooks": { 184 | "pre-push": "npx branch-name-lint [sample-configuration.js]" 185 | } 186 | }, 187 | ``` 188 | 189 | ## GitHub Actions Usage 190 | 191 | You can integrate branch-name-lint into your GitHub Actions workflows to enforce branch naming conventions across your team. This is especially useful for maintaining consistent branch naming in collaborative projects. 192 | 193 | ### Basic Example 194 | 195 | Create a workflow file at `.github/workflows/branch-name-lint.yml`: 196 | 197 | ```yaml 198 | name: Branch Name Lint 199 | 200 | on: 201 | push: 202 | branches-ignore: 203 | - main 204 | - master 205 | pull_request: 206 | branches: 207 | - main 208 | - master 209 | 210 | jobs: 211 | lint-branch-name: 212 | runs-on: ubuntu-latest 213 | steps: 214 | - uses: actions/checkout@v3 215 | - uses: actions/setup-node@v3 216 | with: 217 | node-version: '18' 218 | - run: npm install branch-name-lint --no-save 219 | - name: Extract branch name 220 | shell: bash 221 | run: | 222 | if [ "${{ github.event_name }}" == "pull_request" ]; then 223 | # For pull requests, use the head branch name 224 | echo "BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV 225 | else 226 | # For pushes, extract from GITHUB_REF 227 | echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV 228 | fi 229 | - name: Check branch name 230 | run: npx branch-name-lint 231 | env: 232 | BRANCH_NAME: ${{ env.BRANCH_NAME }} 233 | ``` 234 | 235 | ### Advanced Example with Custom Configuration 236 | 237 | For more advanced use cases, create a custom configuration file: 238 | 239 | 1. First, create a config file at `.github/branch-name-lint.json`: 240 | 241 | ```json 242 | { 243 | "branchNameLinter": { 244 | "prefixes": [ 245 | "feature", 246 | "hotfix", 247 | "release", 248 | "docs", 249 | "chore", 250 | "fix", 251 | "ci", 252 | "test" 253 | ], 254 | "suggestions": { 255 | "features": "feature", 256 | "feat": "feature", 257 | "fix": "hotfix", 258 | "releases": "release" 259 | }, 260 | "banned": ["wip"], 261 | "skip": ["main", "master", "develop", "staging"], 262 | "disallowed": [], 263 | "separator": "/", 264 | "branchNameEnvVariable": "BRANCH_NAME", 265 | "msgBranchBanned": "Branches with the name \"%s\" are not allowed.", 266 | "msgPrefixNotAllowed": "Branch prefix \"%s\" is not allowed.", 267 | "msgPrefixSuggestion": "Instead of \"%s\" try \"%s\".", 268 | "msgSeparatorRequired": "Branch \"%s\" must contain a separator \"%s\"." 269 | } 270 | } 271 | ``` 272 | 273 | 2. Then reference this configuration in your workflow: 274 | 275 | ```yaml 276 | name: Branch Name Lint 277 | 278 | on: 279 | push: 280 | branches-ignore: 281 | - main 282 | - master 283 | pull_request: 284 | branches: 285 | - main 286 | - master 287 | 288 | jobs: 289 | lint-branch-name: 290 | runs-on: ubuntu-latest 291 | steps: 292 | - uses: actions/checkout@v3 293 | - uses: actions/setup-node@v3 294 | with: 295 | node-version: '18' 296 | - run: npm install branch-name-lint --no-save 297 | - name: Extract branch name 298 | shell: bash 299 | run: | 300 | if [ "${{ github.event_name }}" == "pull_request" ]; then 301 | echo "BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV 302 | else 303 | echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV 304 | fi 305 | - name: Check branch name 306 | run: npx branch-name-lint .github/branch-name-lint.json 307 | env: 308 | BRANCH_NAME: ${{ env.BRANCH_NAME }} 309 | ``` 310 | 311 | ### Handling Different Operating Systems 312 | 313 | When using branch-name-lint in a matrix strategy with multiple operating systems, ensure you use environment variables in a cross-platform way: 314 | 315 | ```yaml 316 | jobs: 317 | lint-branch-name: 318 | runs-on: ${{ matrix.os }} 319 | strategy: 320 | matrix: 321 | os: [ubuntu-latest, windows-latest, macos-latest] 322 | steps: 323 | - uses: actions/checkout@v3 324 | - uses: actions/setup-node@v3 325 | with: 326 | node-version: '18' 327 | - run: npm install branch-name-lint --no-save 328 | - name: Extract branch name 329 | shell: bash 330 | run: | 331 | if [ "${{ github.event_name }}" == "pull_request" ]; then 332 | echo "BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV 333 | else 334 | echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV 335 | fi 336 | - name: Check branch name 337 | run: npx branch-name-lint .github/branch-name-lint.json 338 | env: 339 | BRANCH_NAME: ${{ env.BRANCH_NAME }} 340 | ``` 341 | 342 | This setup ensures that your branch naming conventions are enforced consistently across all contributions to your repository. 343 | 344 | ## Usage in Node.js 345 | 346 | ```js 347 | const branchNameLint = require('branch-name-lint'); 348 | 349 | branchNameLint(); 350 | //=> 1 OR 0. 351 | ``` 352 | 353 | ## API 354 | 355 | ### branchNameLint([options]) 356 | 357 | #### options 358 | 359 | Type: `object` 360 | Default: 361 | 362 | ``` 363 | { 364 | prefixes: ['feature', 'hotfix', 'release'], 365 | suggestions: {features: 'feature', feat: 'feature', fix: 'hotfix', releases: 'release'}, 366 | banned: ['wip'], 367 | skip: [], 368 | disallowed: ['master', 'develop', 'staging'], 369 | separator: '/', 370 | msgBranchBanned: 'Branches with the name "%s" are not allowed.', 371 | msgBranchDisallowed: 'Pushing to "%s" is not allowed, use git-flow.', 372 | msgPrefixNotAllowed: 'Branch prefix "%s" is not allowed.', 373 | msgPrefixSuggestion: 'Instead of "%s" try "%s".', 374 | msgSeparatorRequired: 'Branch "%s" must contain a separator "%s".' 375 | } 376 | ``` 377 | 378 | ## License 379 | 380 | MIT © [Ran Bar-Zik](https://internet-israel.com) 381 | -------------------------------------------------------------------------------- /tests/test.js: -------------------------------------------------------------------------------- 1 | // filepath: /Users/spare10/local/open-source/branch-name-lint/tests/test.js 2 | // Replaced AVA with Node.js native assert module 3 | const assert = require('assert'); 4 | const sinon = require('sinon'); 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const BranchNameLint = require('..'); 8 | const childProcess = require('child_process'); // Added missing import for childProcess 9 | 10 | // Track failed tests to exit with proper code 11 | let failedTests = 0; 12 | 13 | // Updated test cases to use assert instead of AVA 14 | function test(description, callback) { 15 | try { 16 | // Make sure to restore sinon stubs before each test 17 | sinon.restore(); 18 | callback(); 19 | console.log(`✔ ${description}`); 20 | } catch (error) { 21 | console.error(`✖ ${description}`); 22 | console.error(error); 23 | failedTests++; // Increment failed test counter 24 | } finally { 25 | // Ensure stubs are restored after each test 26 | sinon.restore(); 27 | } 28 | } 29 | 30 | // Example test case 31 | test('See that constructor have default options', () => { 32 | const branchNameLint = new BranchNameLint(); 33 | assert(branchNameLint.options); 34 | assert.strictEqual(branchNameLint.options.prefixes[0], 'feature'); 35 | }); 36 | 37 | test('See that constructor accept custom options', () => { 38 | const mockOptions = { 39 | prefixes: ['test1', 'test2'], 40 | }; 41 | const branchNameLint = new BranchNameLint(mockOptions); 42 | assert.strictEqual(branchNameLint.options.prefixes[0], 'test1'); 43 | }); 44 | 45 | test('error prints error', () => { 46 | const branchNameLint = new BranchNameLint(); 47 | const callback = sinon.stub(console, 'error'); 48 | const answer = branchNameLint.error('Branch "%s" must contain a separator "%s".', 'test1', 'test2'); 49 | assert(callback); 50 | assert.strictEqual(answer, 1); 51 | callback.restore(); 52 | }); 53 | 54 | test('error handles multiple arguments', () => { 55 | const branchNameLint = new BranchNameLint(); 56 | const callback = sinon.stub(console, 'error'); 57 | const answer = branchNameLint.error('Error: %s, Code: %d', 'Invalid branch', 404); 58 | assert(callback.calledWith('Branch name lint fail!', 'Error: Invalid branch, Code: 404')); 59 | assert.strictEqual(answer, 1); 60 | callback.restore(); 61 | }); 62 | 63 | test('validateWithRegex - fail', () => { 64 | const branchNameLint = new BranchNameLint(); 65 | branchNameLint.options.regex = '^regex.*-test$'; 66 | const validation = branchNameLint.validateWithRegex(); 67 | assert(!validation); 68 | }); 69 | 70 | test('validateWithRegex - pass with correct regex', () => { 71 | sinon.stub(childProcess, 'execFileSync').returns('regex-pattern-test'); 72 | const branchNameLint = new BranchNameLint(); 73 | branchNameLint.options.regex = '^regex.*-test$'; 74 | const validation = branchNameLint.validateWithRegex(); 75 | assert(validation); 76 | childProcess.execFileSync.restore(); 77 | }); 78 | 79 | test('validateWithRegex and options', () => { 80 | sinon.stub(childProcess, 'execFileSync').returns('REGEX-PATTERN-TEST'); 81 | const branchNameLint = new BranchNameLint(); 82 | branchNameLint.options.regex = '^regex.*-test$'; 83 | branchNameLint.options.regexOptions = 'i'; 84 | const validation = branchNameLint.validateWithRegex(); 85 | assert(validation); 86 | childProcess.execFileSync.restore(); 87 | }); 88 | 89 | test('validateWithRegex - invalid regex pattern', () => { 90 | const branchNameLint = new BranchNameLint(); 91 | branchNameLint.options.regex = '['; // Invalid regex pattern 92 | assert.throws(() => branchNameLint.validateWithRegex(), SyntaxError); 93 | }); 94 | 95 | test('getCurrentBranch is working', () => { 96 | const branchNameLint = new BranchNameLint(); 97 | sinon.stub(childProcess, 'execFileSync').returns('branch mock name'); 98 | const name = branchNameLint.getCurrentBranch(); 99 | assert.strictEqual(name, 'branch mock name'); 100 | childProcess.execFileSync.restore(); 101 | }); 102 | 103 | test('doValidation is working', () => { 104 | sinon.stub(childProcess, 'execFileSync').returns('feature/valid-name'); 105 | const branchNameLint = new BranchNameLint(); 106 | const result = branchNameLint.doValidation(); 107 | assert.strictEqual(result, branchNameLint.SUCCESS_CODE); 108 | childProcess.execFileSync.restore(); 109 | }); 110 | 111 | test('doValidation is throwing error on prefixes', () => { 112 | sinon.stub(childProcess, 'execFileSync').returns('blah/valid-name'); 113 | const branchNameLint = new BranchNameLint(); 114 | const result = branchNameLint.doValidation(); 115 | assert.strictEqual(result, branchNameLint.ERROR_CODE); 116 | childProcess.execFileSync.restore(); 117 | }); 118 | 119 | test('doValidation is throwing error on separator', () => { 120 | sinon.stub(childProcess, 'execFileSync').returns('feature-valid-name'); 121 | const branchNameLint = new BranchNameLint(); 122 | const result = branchNameLint.doValidation(); 123 | assert.strictEqual(result, branchNameLint.ERROR_CODE); 124 | childProcess.execFileSync.restore(); 125 | }); 126 | 127 | test('doValidation is throwing error on disallowed', () => { 128 | sinon.stub(childProcess, 'execFileSync').returns('master'); 129 | const branchNameLint = new BranchNameLint(); 130 | const result = branchNameLint.doValidation(); 131 | assert.strictEqual(result, branchNameLint.ERROR_CODE); 132 | childProcess.execFileSync.restore(); 133 | }); 134 | 135 | test('doValidation is throwing error on banned', () => { 136 | sinon.stub(childProcess, 'execFileSync').returns('wip'); 137 | const branchNameLint = new BranchNameLint(); 138 | const result = branchNameLint.doValidation(); 139 | assert.strictEqual(result, branchNameLint.ERROR_CODE); 140 | childProcess.execFileSync.restore(); 141 | }); 142 | 143 | test('doValidation is using correct error message on regex mismatch', () => { 144 | sinon.stub(childProcess, 'execFileSync').returns('feature/invalid_characters'); 145 | const branchNameLint = new BranchNameLint({ 146 | msgDoesNotMatchRegex: 'my error message', 147 | regex: '^[a-z0-9/-]+$', 148 | regexOptions: 'i', 149 | }); 150 | const errorStub = sinon.stub(branchNameLint, 'error'); 151 | branchNameLint.doValidation(); 152 | assert(errorStub.calledWith('my error message', 'feature/invalid_characters', '^[a-z0-9/-]+$')); 153 | errorStub.restore(); 154 | childProcess.execFileSync.restore(); 155 | }); 156 | 157 | test('doValidation is passing on skip', () => { 158 | sinon.stub(childProcess, 'execFileSync').returns('develop'); 159 | const mockOptions = { 160 | skip: ['develop'], 161 | }; 162 | const branchNameLint = new BranchNameLint(mockOptions); 163 | const result = branchNameLint.doValidation(); 164 | assert.strictEqual(result, branchNameLint.SUCCESS_CODE); 165 | childProcess.execFileSync.restore(); 166 | }); 167 | 168 | test('doValidation applies suggestions', () => { 169 | sinon.stub(childProcess, 'execFileSync').returns('feat/valid-name'); 170 | const branchNameLint = new BranchNameLint(); 171 | const result = branchNameLint.doValidation(); 172 | assert.strictEqual(result, branchNameLint.ERROR_CODE); 173 | childProcess.execFileSync.restore(); 174 | }); 175 | 176 | // Add tests for disabling separator and prefix checks 177 | test('doValidation allows branch without separator when separator is set to false', () => { 178 | sinon.stub(childProcess, 'execFileSync').returns('feature-valid-name'); 179 | const branchNameLint = new BranchNameLint({ 180 | separator: false 181 | }); 182 | const result = branchNameLint.doValidation(); 183 | assert.strictEqual(result, branchNameLint.SUCCESS_CODE); 184 | }); 185 | 186 | test('doValidation allows any prefix when prefixes is set to false', () => { 187 | sinon.stub(childProcess, 'execFileSync').returns('custom/valid-name'); 188 | const branchNameLint = new BranchNameLint({ 189 | prefixes: false 190 | }); 191 | const result = branchNameLint.doValidation(); 192 | assert.strictEqual(result, branchNameLint.SUCCESS_CODE); 193 | }); 194 | 195 | test('doValidation allows both prefix and separator to be disabled', () => { 196 | sinon.stub(childProcess, 'execFileSync').returns('anything-goes'); 197 | const branchNameLint = new BranchNameLint({ 198 | prefixes: false, 199 | separator: false 200 | }); 201 | const result = branchNameLint.doValidation(); 202 | assert.strictEqual(result, branchNameLint.SUCCESS_CODE); 203 | }); 204 | 205 | // Add tests for JavaScript configuration support 206 | test('loadConfiguration can load JavaScript configuration files', () => { 207 | // Create a mock implementation of parts of the bin/branch-name-lint module 208 | const path = require('path'); 209 | const fs = require('fs'); 210 | 211 | // Save original implementations 212 | const originalReadFileSync = fs.readFileSync; 213 | const originalRequire = require; 214 | 215 | // Create test class similar to what's in bin/branch-name-lint 216 | class TestLoader { 217 | constructor() { 218 | this.options = this.loadConfiguration('sample-configuration.js'); 219 | } 220 | 221 | loadConfiguration(filename) { 222 | const fileExtension = path.extname(filename); 223 | if (fileExtension === '.js') { 224 | // Test the JS loading path 225 | return { 226 | prefixes: ['feature', 'bugfix', 'hotfix', 'release', 'ci', 'build'], 227 | banned: ['wip', 'tmp'] 228 | }; 229 | } else { 230 | // This is the JSON path 231 | return { prefixes: ['feature'] }; 232 | } 233 | } 234 | } 235 | 236 | // Create an instance and test 237 | const loader = new TestLoader(); 238 | assert(loader.options); 239 | assert(Array.isArray(loader.options.prefixes)); 240 | assert.strictEqual(loader.options.prefixes.length, 6); 241 | assert(loader.options.prefixes.includes('ci')); 242 | }); 243 | 244 | test('JavaScript configuration supports importing constants', () => { 245 | // This test verifies the actual sample-configuration.js file contains expected values 246 | const jsConfig = require('./sample-configuration.js'); 247 | 248 | // Check that the configuration has the combined prefix values 249 | assert(Array.isArray(jsConfig.prefixes)); 250 | assert(jsConfig.prefixes.includes('feature')); 251 | assert(jsConfig.prefixes.includes('bugfix')); 252 | assert(jsConfig.prefixes.includes('ci')); 253 | assert(jsConfig.prefixes.includes('build')); 254 | assert.strictEqual(jsConfig.prefixes.length, 6); // Should have all 6 prefixes 255 | 256 | // Check that the other config values exist 257 | assert(jsConfig.suggestions); 258 | assert.strictEqual(jsConfig.suggestions.feat, 'feature'); 259 | assert(Array.isArray(jsConfig.banned)); 260 | assert(jsConfig.banned.includes('wip')); 261 | }); 262 | 263 | // Add tests for the new branch name features 264 | test('getCurrentBranch uses the provided branch option if available', () => { 265 | const branchNameLint = new BranchNameLint({ 266 | branch: 'feature/custom-branch-option' 267 | }); 268 | 269 | // Make sure execFileSync is stubbed so we know it's not calling git 270 | const execStub = sinon.stub(childProcess, 'execFileSync'); 271 | execStub.returns('this-should-not-be-used'); 272 | 273 | const name = branchNameLint.getCurrentBranch(); 274 | assert.strictEqual(name, 'feature/custom-branch-option'); 275 | 276 | // Verify that execFileSync wasn't called since we provided a branch name 277 | assert(execStub.notCalled); 278 | execStub.restore(); 279 | }); 280 | 281 | test('getCurrentBranch uses the environment variable if branchNameEnvVariable is set', () => { 282 | // Save the original process.env 283 | const originalEnv = process.env; 284 | 285 | // Set up a test environment variable 286 | process.env = { 287 | ...originalEnv, 288 | TEST_BRANCH_NAME: 'feature/from-env-var' 289 | }; 290 | 291 | const branchNameLint = new BranchNameLint({ 292 | branchNameEnvVariable: 'TEST_BRANCH_NAME' 293 | }); 294 | 295 | // Make sure execFileSync is stubbed so we know it's not calling git 296 | const execStub = sinon.stub(childProcess, 'execFileSync'); 297 | execStub.returns('this-should-not-be-used'); 298 | 299 | const name = branchNameLint.getCurrentBranch(); 300 | assert.strictEqual(name, 'feature/from-env-var'); 301 | 302 | // Verify that execFileSync wasn't called since we used the env var 303 | assert(execStub.notCalled); 304 | 305 | // Restore the original process.env and stub 306 | process.env = originalEnv; 307 | execStub.restore(); 308 | }); 309 | 310 | test('getCurrentBranch respects option priority: branch > branchNameEnvVariable > git', () => { 311 | // Save the original process.env 312 | const originalEnv = process.env; 313 | 314 | // Set up a test environment variable 315 | process.env = { 316 | ...originalEnv, 317 | TEST_BRANCH_NAME: 'feature/from-env-var' 318 | }; 319 | 320 | // Create with both branch and branchNameEnvVariable set 321 | const branchNameLint = new BranchNameLint({ 322 | branch: 'feature/from-option', 323 | branchNameEnvVariable: 'TEST_BRANCH_NAME' 324 | }); 325 | 326 | // Make sure execFileSync is stubbed so we know it's not calling git 327 | const execStub = sinon.stub(childProcess, 'execFileSync'); 328 | execStub.returns('feature/from-git'); 329 | 330 | const name = branchNameLint.getCurrentBranch(); 331 | 332 | // Should use the branch option, not the env var or git 333 | assert.strictEqual(name, 'feature/from-option'); 334 | 335 | // Verify that execFileSync wasn't called 336 | assert(execStub.notCalled); 337 | 338 | // Restore the original process.env and stub 339 | process.env = originalEnv; 340 | execStub.restore(); 341 | }); 342 | 343 | test('doValidation works with custom branch name from option', () => { 344 | // No need to stub execFileSync as it shouldn't be called 345 | const branchNameLint = new BranchNameLint({ 346 | branch: 'feature/valid-from-option' 347 | }); 348 | 349 | const result = branchNameLint.doValidation(); 350 | assert.strictEqual(result, branchNameLint.SUCCESS_CODE); 351 | }); 352 | 353 | test('doValidation rejects invalid custom branch name from option', () => { 354 | // No need to stub execFileSync as it shouldn't be called 355 | const branchNameLint = new BranchNameLint({ 356 | branch: 'invalid-branch-name' 357 | }); 358 | 359 | const result = branchNameLint.doValidation(); 360 | assert.strictEqual(result, branchNameLint.ERROR_CODE); 361 | }); 362 | 363 | test('branchNameEnvVariable defaults to false', () => { 364 | const branchNameLint = new BranchNameLint(); 365 | assert.strictEqual(branchNameLint.options.branchNameEnvVariable, false); 366 | }); 367 | 368 | test('branch option defaults to false', () => { 369 | const branchNameLint = new BranchNameLint(); 370 | assert.strictEqual(branchNameLint.options.branch, false); 371 | }); 372 | 373 | // Exit with non-zero status code if any tests failed 374 | process.on('exit', () => { 375 | if (failedTests > 0) { 376 | console.error(`\n${failedTests} test(s) failed`); 377 | process.exit(1); 378 | } else { 379 | console.log('\nAll tests passed!'); 380 | } 381 | }); 382 | --------------------------------------------------------------------------------