Skip to content

fix: Silently succeeds in shallow clones, missing invalid commits #4555

@CervEdin

Description

@CervEdin

Steps to Reproduce

This reproduces the GitHub Actions scenario where actions/checkout@v4 uses --depth=1 by default.

Quick reproduction using test branch:

# 1. Create a shallow clone (simulates GitHub Actions checkout)
rm -rf /tmp/commitlint-bug-repro && git init /tmp/commitlint-bug-repro
cd /tmp/commitlint-bug-repro
git remote add origin https://github.com/CervEdin/commitlint.git
git fetch --no-tags --depth=1 origin test-commitlint-shallow-bug
git checkout FETCH_HEAD

# 2. Fetch master branch (shallow)
git fetch --depth=1 origin master

# 3. Verify incomplete history
git merge-base origin/master HEAD || echo "No merge-base found (shallow clone)"
git log origin/master..HEAD --oneline
# Shows only 1 commit: "fix: resolve issue with shallow clones"

# 4. Create simple config (to avoid dependency issues)
cat > /tmp/simple-config.js << 'EOF'
export default {
  rules: {
    'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']],
    'type-empty': [2, 'never'],
    'subject-empty': [2, 'never']
  }
};
EOF

# 5. Run commitlint - it will PASS despite missing invalid commits
npx @commitlint/cli@latest --config /tmp/simple-config.js --from origin/master --to HEAD --verbose
# Result: ✔ found 0 problems, 0 warnings (FALSE POSITIVE!)

Compare with full clone:

# Clone with full history
git clone https://github.com/CervEdin/commitlint.git /tmp/commitlint-full
cd /tmp/commitlint-full
git checkout test-commitlint-shallow-bug

# Check actual commits in range
git log origin/master..HEAD --oneline
# Shows 4 commits:
# - fix: resolve issue with shallow clones ✓
# - adapt thingyBeepBoop ✗ (INVALID)
# - update some documentation ✗ (INVALID)
# - feat: add initial feature for testing ✓

# Run commitlint with full history
npx @commitlint/cli@latest --config /tmp/simple-config.js --from origin/master --to HEAD --verbose
# Result: ✖ found 2 problems, 0 warnings (for each invalid commit)
# Correctly identifies both invalid commits

Key observation: In shallow clone, commitlint validates only 1 commit and passes. In full clone, it correctly identifies 2 invalid commits out of 4 total.

Current Behavior

When running commitlint --from origin/master --to HEAD in a shallow clone,
commitlint silently succeeds even when commits in the specified range don't
exist in the repository, potentially missing invalid commit messages.

Commitlint validates only the commits that exist in the shallow clone and
silently succeeds, giving a false positive that all commits in the range are
valid.

Real-world example from our CI pipeline:

Using GitHub Actions with actions/checkout@v4 (which uses --depth=1):

  • Branch had 6 commits from origin/master to HEAD
  • One commit had invalid message: "adapt thingyBeepBoop" (no type prefix)
  • Shallow clone contained only HEAD commit (which was valid)
  • Commitlint validated only HEAD, reported success
  • Invalid commit was never checked

Expected Behavior

Commitlint should verify that a merge-base exists between --from and --to, and fail with a clear error when the history is incomplete. Something like:

Error: Cannot find merge-base between origin/master and HEAD
This typically indicates incomplete git history (e.g., shallow clone).

This follows git's own pattern - git diff --three-dot fails with "no merge base" when history is incomplete.

Affected packages

  • cli
  • core
  • prompt
  • config-angular

Possible Solution

Check if a merge-base exists between --from and --to before validation:

git merge-base "$from" "$to" >/dev/null 2>&1 || {
  echo "Error: Cannot find merge-base between $from and $to"
  echo "This typically indicates incomplete git history (e.g., shallow clone)"
  exit 1
}

This is more precise than checking for shallow clones because:

  • It detects the actual problem (incomplete history between specified refs)
  • Git itself uses merge-base for three-dot diff (git diff --three-dot)
  • It's reasonable for a validation tool to require a complete commit range

Workaround for users:

In GitHub Actions, fetch sufficient history and verify it's complete:

Recommended: Fetch depth and verify with merge-base:

- uses: actions/checkout@v4
  with:
    fetch-depth: 100  # Covers most PRs

- name: Commitlint
  run: |
    # Verify we have complete history for the range
    if ! git merge-base origin/master HEAD >/dev/null 2>&1; then
      echo "Insufficient history, fetching more..."
      git fetch --unshallow origin
    fi
    npx commitlint --from origin/master --to HEAD

This approach:

  • Fetches 100 commits upfront (covers most PRs)
  • Falls back to full history only when needed (PRs >100 commits)
  • Uses git merge-base to detect incomplete history

Alternative: Fetch full history (simpler but slower):

- uses: actions/checkout@v4
  with:
    fetch-depth: 0 # Fetch full history (0 = unlimited depth)

Context

This issue affects CI pipelines that use shallow clones (the default in GitHub
Actions). Developers expect commitlint to validate all commits in a PR, but it
silently passes when it can only see HEAD, creating a false sense of security.

This appears to be related to:

Both show similar patterns where --from and --to silently succeed when they
should fail or warn.

This issue was identified and documented with assistance from
Claude Code.

commitlint --version

@commitlint/cli@20.1.0

git --version

git version 2.51.0

node --version

v24.7.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions