0001. Conventional commit validation
Date: 2026-07-17 Author: Henry Dennis PRD: Conventional commit validation UX design: not yet available
Overview & Scope
EOS will add one lockfile-pinned Commitlint policy to the existing developer
toolchain. Lefthook will validate the commit-message file synchronously during
local commit and amend operations. A dedicated GitHub Actions workflow will
validate the pull-request title used by squash merging and report invalid
commits introduced directly on main. All entry points will load the same root
configuration, which extends @commitlint/config-conventional without custom
rules or overrides.
Non-goals:
- Validate intermediate commits on pull-request branches; the final squash title is the CI merge boundary.
- Implement EOS-owned parsing, lint rules, plugins, formatters, or ignore functions.
- Accept every message allowed by the base Conventional Commits 1.0.0 specification; the selected preset is intentionally stricter.
- Rewrite historical commits or repair messages automatically.
- Make local Git hooks impossible to bypass.
- Add an interactive commit authoring tool such as Commitizen.
- Distribute this configuration to repositories outside EOS.
Architecture
Commitlint runs as a root pnpm development dependency inside the existing EOS workspace. Lefthook invokes it synchronously from local Git, while GitHub Actions installs the same lockfile and invokes the same CLI for repository events. No service, persistent process, network API, or third-party validation Action is introduced.
C4Context Person(contributor, "EOS contributor") System_Ext(git, "Local Git client", "Creates and amends commits") System(eos, "EOS workspace", "Lefthook and lockfile-pinned Commitlint configuration") System_Ext(github, "GitHub Actions", "Runs required pull-request and main-branch checks")
Rel(contributor, git, "Creates a commit") Rel(git, eos, "Runs commit-msg synchronously with the message file") Rel(github, eos, "Checks out and runs Commitlint synchronously with a title or Git range")The local hook is an early-feedback boundary and remains bypassable. The
required GitHub status check is the authoritative boundary for the normal
squash-merge workflow. A push check on main is detective rather than
preventive because it runs after Git accepts the push.
Data Model & API Contracts
Schema changes
No database, persisted schema, migration, or backfill is required. Commitlint configuration, workflow definitions, tests, and documentation are stored as version-controlled repository files.
API contracts
No HTTP, event, or public package API is added. The implementation uses these command-line input contracts:
| Entry point | Input | Type | Required | Output |
|---|---|---|---|---|
Local commit-msg | {1} | path to UTF-8 commit-message file | yes | Commitlint diagnostics and process exit code |
| Pull-request CI | standard input | UTF-8 pull-request title plus newline | yes | GitHub job log and process exit code |
main push CI | --from and --to | Git commit SHAs | yes | Diagnostics for messages in the selected range and process exit code |
| Manual latest-commit check | --last | Git revision selected by Commitlint | yes | Diagnostics and process exit code |
Exit code 0 means the message passed or matched a Commitlint default ignore. A
non-zero exit means at least one non-ignored message violated an error-level
preset rule. Warning-only results remain successful because EOS does not
override preset severities.
Affected Files & Sequence Flows
Affected files
package.json— add the Commitlint development dependencies.pnpm-lock.yaml— pin the resolved Commitlint dependency graph.commitlint.config.mjs— extend@commitlint/config-conventionalwithout any rule or behavior overrides.lefthook.yml— add thecommit-msgcommandpnpm exec commitlint --edit {1}..github/workflows/commitlint.yml— validate pull-request titles from the trusted default-branch context and commits introduced onmainwith one stable status-check name.docs/eos/how-to/run-git-hook-checks.md— document local, manual, and CI validation and correction steps.docs/eos/prds/conventional-commit-validation.md— define the selected preset as the product policy.docs/eos/adrs/0006-use-commitlint-conventional-configuration.md— record the tooling decision and rejected alternatives.docs/eos/tsds/0001-conventional-commit-validation-tsd.md— describe this implementation design.
Sequence: local commit validation
sequenceDiagram participant Contributor participant Git participant Lefthook participant Commitlint
Contributor->>Git: git commit or git commit --amend Git->>Lefthook: commit-msg <message-file> Lefthook->>Commitlint: pnpm exec commitlint --edit <message-file> alt message passes or is ignored by Commitlint Commitlint-->>Lefthook: exit 0 Lefthook-->>Git: continue Git-->>Contributor: create commit else message violates an error-level preset rule Commitlint-->>Lefthook: diagnostics and non-zero exit Lefthook-->>Git: stop Git-->>Contributor: commit not created; edit and retry endSequence: pull-request title validation
sequenceDiagram participant Contributor participant GitHub participant Actions as GitHub Actions participant Commitlint
Contributor->>GitHub: open, update, reopen, or retitle pull request GitHub->>Actions: pull_request_target event Actions->>Actions: check out trusted default branch and install frozen lockfile Actions->>Commitlint: pass PR title through environment and standard input alt title passes or is ignored by Commitlint Commitlint-->>Actions: exit 0 Actions-->>GitHub: required check passes else title violates an error-level preset rule Commitlint-->>Actions: diagnostics and non-zero exit Actions-->>GitHub: required check fails GitHub-->>Contributor: merge remains blocked endSequence: main-branch push reporting
sequenceDiagram participant GitHub participant Actions as GitHub Actions participant Git participant Commitlint
GitHub->>Actions: push to main with before and after SHAs Actions->>Git: fetch history Actions->>Commitlint: validate --from before --to after alt every selected message passes or is ignored Commitlint-->>Actions: exit 0 Actions-->>GitHub: check passes else any selected message violates an error-level rule Commitlint-->>Actions: diagnostics and non-zero exit Actions-->>GitHub: post-push check fails endTrade-off Analysis
Commitlint conventional preset — chosen
@commitlint/cli with @commitlint/config-conventional supplies file, standard
input, and Git-range modes plus maintained diagnostics. Extending the preset
without overrides satisfies the requirement not to own Commitlint rules and
keeps local and CI behavior tied to the same installed dependency graph. EOS
accepts the preset’s fixed types, subject constraints, 100-character limits,
warnings, and default ignores as the cost of avoiding custom policy code.
Commitlint with EOS-owned rules — rejected
An EOS-owned configuration could match the base Conventional Commits 1.0.0 syntax more literally. It would require custom rules or a local plugin for parser and footer edge cases, creating exactly the maintenance surface the revised requirement excludes.
Conventional Commits reference parser with an EOS CLI — rejected
The reference parser supplies a syntax tree but not the required diagnostics, Git-range loading, hook integration, or policy exit behavior. Building those parts would replace a small configuration with a larger custom validator and would not meet the intent behind rejecting custom Commitlint rules.
Separate GitHub validation Action — rejected
A purpose-built Action can simplify event handling, but it would create a second policy and upgrade boundary beside the local CLI. Using the installed Commitlint CLI directly provides the needed behavior without exposing event data to another Action dependency.
The durable tooling choice is recorded in ADR 0006.
Non-Functional Requirements
Performance & scale
- Local validation must finish within one second for a commit message of up to 10 KiB when run in the EOS Nix development shell on a supported developer machine.
- CI must validate a normal pull-request title without increasing the existing CI critical path by more than one minute, including checkout and dependency installation.
- A
mainpush range of up to 100 commits must complete within the workflow’s five-minute timeout.
Security & privacy
- Pull-request titles are untrusted input and must be passed through an environment variable and standard input, never interpolated into a generated shell command.
- Pull-request title validation must run from
pull_request_targetusing only the trusted default-branch checkout. It must never fetch or execute the pull request head. - Commit messages must not be uploaded to a third-party validation service; they remain in local process memory or the existing GitHub Actions logs.
- The workflow must use the repository’s frozen lockfile and read-only
contentspermission. - The validation job must not receive repository secrets or request write permissions.
No new user-facing UI is introduced, so the accessibility baseline is unaffected.
Error Handling & Edge Cases
Failure behavior
- If local Commitlint returns non-zero, Lefthook propagates that exit status and Git aborts the commit without changing the index or message file.
- If dependency installation or Commitlint execution fails in pull-request CI, the required status check fails closed and merging remains blocked.
- If Commitlint finds one or more invalid commits in a
mainpush range, the job reports all diagnostics available from the CLI and fails; it does not attempt to rewrite published history. - A later
mainpush must not cancel an in-progress push validation because each workflow run validates a distinct commit range that must complete. - If the Git
beforeSHA is unavailable or is the all-zero SHA, the push job validates the newHEADcommit and emits a log message explaining the fallback. - If a Git range cannot be resolved after a full-history checkout, the job fails explicitly rather than silently skipping validation.
Idempotency
- Validation is read-only and naturally idempotent: rerunning it with the same message, configuration, lockfile, and Git range produces the same result.
- Editing a pull-request title triggers a new check; the latest completed result replaces the previous title’s relevance without mutating either title.
Known edge cases
- Empty, malformed, uppercase-type, disallowed-type, terminal-full-stop, and over-100-character headers or non-URL body/footer lines fail according to the preset.
- Warning-only body or footer blank-line violations log warnings and pass because EOS does not change preset severities.
- Merge, revert, version, automatic-merge,
fixup!, andsquash!messages recognized by Commitlint’s default ignores pass without rule evaluation. git commit --no-verifybypasses the local hook; pull-request title CI still protects the normal merge path.- Special characters in a pull-request title are treated as data because the title is supplied through an environment variable and standard input.
- A title edit must trigger validation even when no branch commit changes, so
the workflow subscribes to the
editedpull-request action. - Merge-queue events do not provide a new pull-request title to validate; the workflow publishes a successful status only after the pull request has already passed its title check.
Testing Strategy
Unit
No unit tests are added. EOS does not implement commit parsing or lint rules; those behaviors belong to the pinned Commitlint packages.
Integration
No automated Commitlint integration suite is added. Implementation verification
checks that the root configuration resolves, Lefthook can pass Git’s
worktree-aware COMMIT_EDITMSG path to Commitlint, and the GitHub Actions
workflow is syntactically valid.
Manual verification
- GitHub required-check and squash-title settings are repository-hosted
configuration not represented fully in the checkout. Before enabling the gate,
open a draft pull request with an invalid title, confirm the named check
fails, retitle it to
docs: verify commit validation, and confirm the same check passes. - Git GUI clients may launch hooks outside the Nix shell. Test one supported GUI client after installation and confirm it either runs Commitlint successfully or presents the documented environment requirement.
Rollout & Rollback Plan
Rollout sequence
- Add the pinned Commitlint CLI and conventional preset dependencies and root configuration.
- Add the Lefthook
commit-msgcommand and verify valid, invalid, amended, and default-ignored local messages inside the Nix shell. - Update the Git-hook how-to guide with policy, manual commands, diagnostics, and repair steps.
- Add the dedicated GitHub Actions workflow for pull-request titles,
merge-group compatibility, and
mainpush ranges without making its status required yet. - Run the workflow on a test pull request, including a title-only edit from an invalid title to a valid title.
- Verify GitHub squash-merge settings use the pull-request title as the final commit subject.
- Configure the stable Commitlint job name as a required status check for
mainand the merge queue, if enabled. - Confirm the existing Changesets release pull request title and commit pass before considering rollout complete.
No feature flag or data migration is required.
Rollback path
- Remove the Commitlint status check from the
mainrepository ruleset first so open pull requests are not blocked by a disappearing workflow. - Revert the GitHub Actions workflow and confirm the check is no longer expected by the merge queue.
- Revert the Lefthook
commit-msgcommand to stop local enforcement. - Remove
commitlint.config.mjs, the dependencies, and lockfile entries. - Revert the validation-specific documentation while retaining the original
conventional-commit guidance in
AGENTS.md.
Rollback does not require history repair because validation stores no data and does not mutate existing commits.
Observability
Logging
- Local failures use Commitlint’s standard output, including the message and each failing rule name; Lefthook preserves the output in the terminal.
- GitHub Actions preserves the same diagnostics in the named validation job.
- The
mainpush path logs the selectedbeforeandafterSHAs or the single-commit fallback before invoking Commitlint. - Workflow setup and range-resolution failures emit explicit GitHub error annotations rather than appearing as successful skips.
Metrics
- GitHub Actions status provides a pass/fail result for every subscribed
pull-request and
mainpush event. - No long-term telemetry store or dashboard is introduced; historical workflow runs remain the source for validation outcomes.
Alerts & dashboards
- No paging alert is warranted for a developer workflow. A failed required check is immediately visible to the pull-request author and repository maintainers and blocks merging.
- Repeated infrastructure failures are investigated from GitHub Actions history when two consecutive runs of the same commit fail before Commitlint executes; repository maintainers own that response.
Dependencies & Open Risks
Dependencies
- Blocked on: Repository-admin access to verify squash-merge behavior and add the required status check. Code and documentation work are not otherwise blocked.
- Blocks: Reliable machine-readable final history and any future automation that assumes conventional squash titles.
- Third-party:
@commitlint/cli,@commitlint/config-conventional, their lockfile-pinned transitive dependencies, Lefthook, Git, pnpm, Node.js from the Nix shell, and GitHub Actions.
Open risks
- Confirm which repository administrator will apply and own the required-check and squash-merge settings.
- Confirm whether merge queue is enabled; if it is, verify the stable check name
is emitted for
merge_groupevents without revalidating an unavailable pull request title. - Confirm which Git GUI clients are supported for EOS contributors outside an interactive Nix shell.
- Dependency updates can change preset behavior and must be reviewed as policy changes rather than accepted as routine lockfile churn.