Skip to content

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 pointInputTypeRequiredOutput
Local commit-msg{1}path to UTF-8 commit-message fileyesCommitlint diagnostics and process exit code
Pull-request CIstandard inputUTF-8 pull-request title plus newlineyesGitHub job log and process exit code
main push CI--from and --toGit commit SHAsyesDiagnostics for messages in the selected range and process exit code
Manual latest-commit check--lastGit revision selected by CommitlintyesDiagnostics 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-conventional without any rule or behavior overrides.
  • lefthook.yml — add the commit-msg command pnpm exec commitlint --edit {1}.
  • .github/workflows/commitlint.yml — validate pull-request titles from the trusted default-branch context and commits introduced on main with 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
end

Sequence: 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
end

Sequence: 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
end

Trade-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 main push 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_target using 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 contents permission.
  • 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 main push range, the job reports all diagnostics available from the CLI and fails; it does not attempt to rewrite published history.
  • A later main push must not cancel an in-progress push validation because each workflow run validates a distinct commit range that must complete.
  • If the Git before SHA is unavailable or is the all-zero SHA, the push job validates the new HEAD commit 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!, and squash! messages recognized by Commitlint’s default ignores pass without rule evaluation.
  • git commit --no-verify bypasses 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 edited pull-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

  1. Add the pinned Commitlint CLI and conventional preset dependencies and root configuration.
  2. Add the Lefthook commit-msg command and verify valid, invalid, amended, and default-ignored local messages inside the Nix shell.
  3. Update the Git-hook how-to guide with policy, manual commands, diagnostics, and repair steps.
  4. Add the dedicated GitHub Actions workflow for pull-request titles, merge-group compatibility, and main push ranges without making its status required yet.
  5. Run the workflow on a test pull request, including a title-only edit from an invalid title to a valid title.
  6. Verify GitHub squash-merge settings use the pull-request title as the final commit subject.
  7. Configure the stable Commitlint job name as a required status check for main and the merge queue, if enabled.
  8. 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

  1. Remove the Commitlint status check from the main repository ruleset first so open pull requests are not blocked by a disappearing workflow.
  2. Revert the GitHub Actions workflow and confirm the check is no longer expected by the merge queue.
  3. Revert the Lefthook commit-msg command to stop local enforcement.
  4. Remove commitlint.config.mjs, the dependencies, and lockfile entries.
  5. 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 main push path logs the selected before and after SHAs 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 main push 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_group events 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.