Commit verification for GitHub Repositories
A comprehensive composite action that validates Git commits to ensure they meet security and quality standards. This action enforces two critical requirements: commit signing (GPG/SSH signatures) and conventional commit message formatting, providing automated gates for code integrity and traceability.
Key Capabilities:
- Commit Signature Verification - Ensures commits are cryptographically signed
- Conventional Commit Validation - Enforces standardised commit message format
- Zero Configuration - Works out-of-the-box with sensible defaults
- Customisable - Supports custom commitlint configurations
- Architecture
- Features
- What It Does
- Usage Examples
- Inputs
- Required Permissions
- Configuration
- Outputs
- Troubleshooting
- Contributing
This action performs two sequential validation checks on the most recent commit:
- 🧩 Conventional Commit Validation - Parses commit message against conventional commit specification
- 🔐 Signature Verification - Confirms commit has been cryptographically signed
Both checks must pass for the action to succeed.
📦 Repository Checkout
↓
⚡️ Node.js Setup
↓
⚙️ Install Dependencies
↓
🧩 Conventional Commit Check
├─ ✅ Valid format → Continue
└─ ❌ Invalid format → Fail
↓
🔐 Signature Check
├─ ✅ Signed → Success
└─ ❌ Unsigned → Fail- ✅ Conventional Commits - Enforces
type(scope): subjectformat - ✅ Semantic Types - Validates commit types (feat, fix, docs, etc.)
- ✅ Custom Rules - Supports custom commitlint configurations
- ✅ Clear Error Messages - Detailed feedback for invalid messages
- ✅ GPG Signatures - Detects GPG-signed commits
- ✅ SSH Signatures - Supports SSH commit signing
- ✅ No Key Required - Checks signature presence, not validity
- ✅ Fast Validation - Lightweight verification process
- ✅ Automated Gates - Prevents non-compliant commits from merging
- ✅ Audit Trail - Ensures commit traceability and authorship
- ✅ CI/CD Integration - Works with any GitHub Actions workflow
Action: sca/repository/
Purpose: Secure code retrieval with full Git history
- Clones repository with complete commit history
- Authenticates using provided GitHub token
- Preserves Git metadata for signature verification
Purpose: Installs Node.js runtime for commitlint
- Installs specified Node.js version (default: 24.11.1)
- Configures npm environment
- Prepares runtime for commitlint execution
Purpose: Installs commitlint and dependencies
- Runs
npm cifor reproducible installation - Uses
--ignore-scriptsfor security - Installs from action's local
package.json
Purpose: Enforces conventional commit message format
- Extracts commit message from most recent commit
- Validates against conventional commit specification
- Checks for required format:
type(scope): subject - Validates commit types (feat, fix, docs, refactor, etc.)
- Customisable: Use
config-fileinput for custom rules - Fails if commit message format is invalid
Expected Format:
<type>(<scope>): <subject>
<body>
<footer>Valid Examples:
feat(auth): add OAuth2 authenticationfix(api): resolve null pointer exceptiondocs(readme): update installation guiderefactor(core)!: restructure module architecture
Invalid Examples:
- ❌
Updated files- No type specified - ❌
fix:- Empty subject - ❌
Feature: new login- Incorrect type capitalisation
Purpose: Ensures commits are cryptographically signed
- Runs
git log --show-signatureon most recent commit - Detects presence of GPG or SSH signatures
- Does not require public key import
- Does not validate trust chain or signature authenticity (only existence)
- Searches for signature indicators:
gpg: Signature made(GPG signatures)BEGIN PGP SIGNATURE(PGP signatures)
- Fails if no signature is detected
What it checks:
- ✅ Commit has been signed (GPG or SSH)
- ❌ Does NOT verify signature validity
- ❌ Does NOT check signing key identity
Validate commits with default settings:
name: Commit Validation
run-name: Commit Validation 🔎
on:
pull_request:
branches: ["**"]
push:
branches: ["main", "develop"]
permissions: {}
jobs:
validate-commit:
name: Validate Commit
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Validate Commit
uses: ministryofjustice/devsecops-actions/github/commit@9babea875cafae0e3b05a5ec5aca76d6b560c42eThis validates:
- ✅ Commit message follows conventional commits format
- ✅ Commit has been cryptographically signed
Advanced configuration with custom commitlint rules:
name: Commit Validation
run-name: Commit Validation 🔎
on:
pull_request:
branches: ["main", "develop", "release/*"]
types: [opened, synchronize, reopened]
push:
branches: ["main", "develop"]
workflow_dispatch:
permissions: {}
jobs:
validate-commit:
name: Validate Commit
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Validate Commit with Custom Config
uses: ministryofjustice/devsecops-actions/github/commit@9babea875cafae0e3b05a5ec5aca76d6b560c42e
with:
config-file: ".github/commitlint.config.js"
node-version: "24.11.1"Validate commits across multiple branches:
name: Commit Validation - Multi-Branch
run-name: Commit Validation 🔎
on:
push:
branches:
- "main"
- "develop"
- "feature/**"
- "bugfix/**"
- "hotfix/**"
permissions: {}
jobs:
validate-commit:
name: Validate Commit
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Validate Commit
uses: ministryofjustice/devsecops-actions/github/commit@9babea875cafae0e3b05a5ec5aca76d6b560c42eEnsure all commits in a PR are compliant:
name: PR Commit Validation
run-name: PR Validation 🔎
on:
pull_request:
types: [opened, synchronize, reopened]
permissions: {}
jobs:
validate-commits:
name: Validate PR Commits
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Validate Latest Commit
uses: ministryofjustice/devsecops-actions/github/commit@9babea875cafae0e3b05a5ec5aca76d6b560c42e
with:
config-file: "commitlint.config.js"All inputs are optional. The action works with zero configuration.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
config-file |
string | No | "" |
Path to custom commitlint configuration file (e.g., commitlint.config.js) |
node-version |
string | No | "24.11.1" |
Node.js version to use for commitlint execution |
Your workflow must explicitly grant these permissions:
| Permission | Level | Purpose |
|---|---|---|
contents |
read | Repository checkout and commit history access |
Example:
permissions:
contents: readCreate a custom commitlint configuration file to tailor validation rules.
Create commitlint.config.js in your repository root:
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat', // New feature
'fix', // Bug fix
'docs', // Documentation changes
'style', // Code style changes (formatting, etc.)
'refactor', // Code refactoring
'perf', // Performance improvements
'test', // Adding or updating tests
'build', // Build system changes
'ci', // CI/CD changes
'chore', // Maintenance tasks
'revert', // Revert previous commit
],
],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'scope-case': [2, 'always', 'lower-case'],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'header-max-length': [2, 'always', 100],
'body-leading-blank': [1, 'always'],
'body-max-line-length': [2, 'always', 160],
'footer-leading-blank': [1, 'always'],
},
};- uses: ministryofjustice/devsecops-actions/github/commit@9babea875cafae0e3b05a5ec5aca76d6b560c42e
with:
config-file: "commitlint.config.js"Require scope:
rules: {
'scope-empty': [2, 'never'], // Scope is mandatory
}Custom types:
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'security', 'deploy'],
],
}Enforce ticket references:
rules: {
'footer-max-line-length': [0], // Disable
'references-empty': [2, 'never'], // Must reference issue/ticket
}The action validates against the Conventional Commits specification.
Format:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]Types:
| Type | Description | Example |
|---|---|---|
feat |
New feature | feat(auth): add JWT authentication |
fix |
Bug fix | fix(api): handle null response |
docs |
Documentation changes | docs: update README installation steps |
style |
Code style (whitespace, formatting) | style: apply prettier formatting |
refactor |
Code change that neither fixes bug nor adds feature | refactor(parser): simplify token logic |
perf |
Performance improvement | perf(database): optimise query execution |
test |
Adding or updating tests | test(user): add integration tests |
build |
Build system or external dependencies | build: upgrade to webpack 5 |
ci |
CI/CD configuration changes | ci: add deployment workflow |
chore |
Maintenance tasks | chore: update dependencies |
revert |
Revert previous commit | revert: revert commit abc123 |
Breaking Changes:
Add ! after type/scope to indicate breaking changes:
feat(api)!: remove deprecated v1 endpoints
BREAKING CHANGE: API v1 endpoints have been removed. Migrate to v2.The action checks for commit signature presence but does not verify signature validity.
Supported Signature Types:
- GPG Signatures - Traditional GPG key signing
- SSH Signatures - Git SSH signing (Git 2.34+)
How to sign commits:
# Generate GPG key
gpg --full-generate-key
# Configure Git
git config --global user.signingkey YOUR_KEY_ID
git config --global commit.gpgsign true
# Sign a commit
git commit -S -m "feat: add new feature"# Generate SSH key
ssh-keygen -t ed25519 -C "your_email@example.com"
# Configure Git
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
# Sign a commit
git commit -S -m "feat: add new feature"Note: The action only checks that a signature exists, not whether it's valid or trusted. This allows validation in CI environments without importing public keys.
This action does not produce artifacts but provides immediate feedback:
- ✅ Exit Code 0 - All validations passed
- ✅ Console Output - Displays commit message and signature info
- ✅ GitHub Check - Green checkmark in PR checks
- ❌ Exit Code 1 - Validation failed
- ❌ Error Messages - Detailed failure explanation
- ❌ GitHub Check - Red X in PR checks
🧩 Conventional Commit Validation
feat(auth): add OAuth2 authentication
✅ Commit message is valid
🔐 Signature Verification
gpg: Signature made Thu 26 Feb 2026 14:15:00 GMT
gpg: using RSA key ED37D6E7EB2CEBF06DF4E75097AAE65204663900
✅ Commit is signed🧩 Conventional Commit Validation
Updated authentication
❌ Commit message validation failed:
⧗ input: Updated authentication
✖ subject may not be empty [subject-empty]
✖ type may not be empty [type-empty]🔐 Signature Verification
feat(auth): add OAuth2 authentication
❌ Commit is unsigned!
Error: Process completed with exit code 1.Symptom:
✖ type may not be empty [type-empty]Cause: Commit message doesn't follow conventional commits format
Solution: Format commit message correctly:
# ✅ Correct
git commit -m "feat(api): add user endpoint"
# ❌ Incorrect
git commit -m "Added user endpoint"Symptom:
❌ Commit is unsigned!Cause: Commit lacks cryptographic signature
Solution: Configure Git signing:
# GPG signing
git config --global commit.gpgsign true
git config --global user.signingkey YOUR_GPG_KEY_ID
# SSH signing
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign trueSymptom (in logs):
gpg: Can't check signature: No public key
❌ Commit is unsigned!Cause: The action correctly detects signature presence but GPG shows warning
Solution: This is expected behaviour. The action checks for "gpg: Signature made" which appears before the "No public key" warning.
If you still get failure, ensure your commits are actually signed locally before pushing.
Symptom:
Error: Cannot find module './commitlint.config.js'Cause: Config file path is incorrect or file doesn't exist
Solution: Verify file exists and path is correct:
# Check file exists
ls -la .github/commitlint.config.js
# Use correct path
config-file: ".github/commitlint.config.js"Symptom:
Error: The engine "node" is incompatible with this moduleCause: Specified Node.js version too old
Solution: Use newer Node.js version:
with:
node-version: "24.11.1"Enable detailed logging for troubleshooting:
env:
ACTIONS_STEP_DEBUG: true
ACTIONS_RUNNER_DEBUG: true
steps:
- name: Validate Commit
uses: ministryofjustice/devsecops-actions/github/commit@9babea875cafae0e3b05a5ec5aca76d6b560c42e# ✅ Recommended: Commit SHA (maximum security and stability)
uses: ministryofjustice/devsecops-actions/github/commit@9babea875cafae0e3b05a5ec5aca76d6b560c42e
# ⚠️ Acceptable: Version tag (updated periodically)
uses: ministryofjustice/devsecops-actions/github/commit@v1.3.0
# ❌ Not recommended: Branch names (security risk)
uses: ministryofjustice/devsecops-actions/github/commit@main# ✅ Good examples
feat(auth): implement JWT token refresh
fix(api): resolve race condition in request handler
docs(readme): clarify installation requirements
refactor(database)!: migrate to PostgreSQL 15
# ❌ Bad examples
Update code # No type
feat: . # Empty subject
FIX: bug # Incorrect capitalisation
feat(Auth): add # Incorrect scope case# ✅ Always sign commits
git commit -S -m "feat: add feature"
# ✅ Configure signing globally
git config --global commit.gpgsign true
# ✅ Use SSH signing for simplicity (Git 2.34+)
git config --global gpg.format ssh
# ❌ Don't skip signing
git commit --no-gpg-sign -m "feat: add feature"See the main repository Contributing Guidelines for details.
# Navigate to action directory
cd github/commit
# Review action definition
cat action.yml
# Validate YAML
npm run validate:yml
# Test locally with act
act push -W .github/workflows/commit-validation.yml- Make changes to
action.yml - Test locally with act or in a test repository
- Update documentation in this README if needed
- Commit changes following conventional commit format
- Sign your commit with GPG or SSH
- Submit pull request with clear description
This project is licensed under the MIT License - see the LICENSE file for full details.
- 📖 Documentation: This README and inline action documentation
- 🐛 Bug Reports: GitHub Issues
- ✨ Feature Requests: GitHub Issues
- 🔒 Security Issues: See Security Policy
This action leverages:
- Commitlint - Conventional commit validation
- Conventional Commits - Commit message specification
- Git - Built-in commit signature verification
- Conventional Commits Specification
- Commitlint Documentation
- Git Commit Signing
- GPG Signing Guide
- SSH Signing Guide
- Main Repository README
Made with ❤️ by the Ministry of Justice UK - OCTO Cyber